diff --git a/about/index.html b/about/index.html index a5f02a82a..bf9f6e8d1 100644 --- a/about/index.html +++ b/about/index.html @@ -1,4 +1,4 @@ -关于本人 | 高深远的博客

关于本人

关于我的简介,出于方便阅读的目的,我已在置顶的欢迎到访:写在前面中写了一些。当然,为了不让这个page没什么作用,我还是留下点东西吧。
如果你对我写的东西或者对我感兴趣,欢迎和我交流与讨论,互相学习、共同进步。
下面是我的联系方式:
先写这些,谢谢!

0%
关于本人 | 高深远的博客

关于本人

关于我的简介,出于方便阅读的目的,我已在置顶的欢迎到访:写在前面中写了一些。当然,为了不让这个page没什么作用,我还是留下点东西吧。
如果你对我写的东西或者对我感兴趣,欢迎和我交流与讨论,互相学习、共同进步。
下面是我的联系方式:
先写这些,谢谢!

0%
anaconda笔记:conda的各种命令行操作 | 高深远的博客

anaconda笔记:conda的各种命令行操作

anaconda是一个开源的包、环境管理器,可以比较有效地配置多个虚拟环境,当python入门到一定程度时,安装anaconda是很必要的。前段时间室友学习python的时候问到过我一些相关的问题,我就在这里简单写一些我知道的以及我搜集到的知识。


环境变量

安装anaconda过程中一个很重要的步骤就是配置环境变量,网上有很多手动添加环境变量的教程,其实很简单,只需添加三个路径,当然更简单的是直接在安装的时候添加到path(可以无视warning)。我想在这里写的是环境变量的概念问题,其实直到不久前帮同学安装我才明白。
环境变量是指在操作系统中用来指定操作系统运行环境的一些参数。当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还会到path中指定的路径去找。这就是为什么不添加C:\Users\用户名\Anaconda3\Scripts到path就无法执行conda命令,因为此时conda.exe无法被找到。


conda与pip

利用conda installpip install命令来安装各种包的过程中,想必你也对两者之间的区别很疑惑,下面我就总结一下我搜集到的相关解答。
简而言之,pip是python包的通用管理器,而conda是一个与语言无关的跨平台环境管理器。对我们而言,最显着的区别可能是这样的:pip在任何环境中安装python包,conda安装在conda环境中装任何包。因此往往conda list的数量会大于pip list。
要注意的是,如果使用conda安装多个环境时,对于同一个包只需要安装一次,有conda集中进行管理。
但是如果使用pip,因为每个环境安装使用的pip在不同的路径下,故会重复安装,而包会从缓存中取。
总的来说,我推荐尽早安装anaconda并且使用conda来管理python的各种包。


升级

我们可以在命令行中或者anaconda prompt中执行命令进行操作。

1
2
3
conda update conda #升级conda
conda update anaconda #升级anaconda前要先升级conda
conda update --all #升级所有包

在升级完成之后,我们可以使用命令来清理一些无用的包以释放一些空间:

1
2
conda clean -p #删除没有用的包
conda clean -t #删除保存下来的压缩文件(.tar)


虚拟环境

conda list命令用于查看conda下的包,而conda env list命令可以用来查看conda创建的所有虚拟环境。

下面就简述一下如何创建这些虚拟环境。
使用如下命令,可以创建一个新的环境:
conda create -n Python27 python=2.7
其中Python27是自定义的一个名称,而python=2.7是一个格式,可以变动等号右边的数字来改变python环境的kernel版本,这里我们安装的是python2.7版本(将于2020年停止维护)。
在anaconda prompt中,我们可以看到我们处在的是base环境下,也就是我安装的python3环境下,我们可以使用下面两个命令来切换环境:

注意:如果直接在ubuntu的命令行中切换环境,需要加上conda,比如conda activate 环境名

在创建环境的过程中,难免会不小心取了个难听的环境名,别担心,我们有方法来删除环境。
conda remove -n 难听的名字 --all
有时候一个环境已经配置好了,但我们想要重命名,这怎么办呢?可以这样办:

1
2
conda create -n 新名字 --clone 老名字
conda remove -n 老名字 --all

把环境添加到jupyter notebook

首先通过activate进入想要添加的环境中,然后安装ipykernel,接下来就可以进行添加了。

1
2
pip install ipykernel
python -m ipykernel install --name Python27 #Python27可以取与环境名不一样的名字,但方便起见建议统一

我们可以使用jupyter kernelspec list来查看已添加到jupyter notebook的kernel。
显示如下:

我们也可以在jupyter notebook中的new或者kernel下查看新环境是否成功添加。
若想删除某个指定的kernel,可以使用命令jupyter kernelspec remove kernel_name来完成。
在这里我想说明一下为什么要分开python的环境。
由于python是不向后兼容的,分开环境可以避免语法版本不一引起的错误,同时这也可以避免工具包安装与调用的混乱。


碰到底线咯 后面没有啦

本文标题:anaconda笔记:conda的各种命令行操作

文章作者:高深远

发布时间:2019年09月13日 - 23:17

最后更新:2020年02月19日 - 17:28

原始链接:https://gsy00517.github.io/anaconda20190913231748/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
anaconda笔记:conda的各种命令行操作 | 高深远的博客

anaconda笔记:conda的各种命令行操作

anaconda是一个开源的包、环境管理器,可以比较有效地配置多个虚拟环境,当python入门到一定程度时,安装anaconda是很必要的。前段时间室友学习python的时候问到过我一些相关的问题,我就在这里简单写一些我知道的以及我搜集到的知识。


环境变量

安装anaconda过程中一个很重要的步骤就是配置环境变量,网上有很多手动添加环境变量的教程,其实很简单,只需添加三个路径,当然更简单的是直接在安装的时候添加到path(可以无视warning)。我想在这里写的是环境变量的概念问题,其实直到不久前帮同学安装我才明白。
环境变量是指在操作系统中用来指定操作系统运行环境的一些参数。当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还会到path中指定的路径去找。这就是为什么不添加C:\Users\用户名\Anaconda3\Scripts到path就无法执行conda命令,因为此时conda.exe无法被找到。


conda与pip

利用conda installpip install命令来安装各种包的过程中,想必你也对两者之间的区别很疑惑,下面我就总结一下我搜集到的相关解答。
简而言之,pip是python包的通用管理器,而conda是一个与语言无关的跨平台环境管理器。对我们而言,最显着的区别可能是这样的:pip在任何环境中安装python包,conda安装在conda环境中装任何包。因此往往conda list的数量会大于pip list。
要注意的是,如果使用conda安装多个环境时,对于同一个包只需要安装一次,有conda集中进行管理。
但是如果使用pip,因为每个环境安装使用的pip在不同的路径下,故会重复安装,而包会从缓存中取。
总的来说,我推荐尽早安装anaconda并且使用conda来管理python的各种包。


升级

我们可以在命令行中或者anaconda prompt中执行命令进行操作。

1
2
3
conda update conda #升级conda
conda update anaconda #升级anaconda前要先升级conda
conda update --all #升级所有包

在升级完成之后,我们可以使用命令来清理一些无用的包以释放一些空间:

1
2
conda clean -p #删除没有用的包
conda clean -t #删除保存下来的压缩文件(.tar)


虚拟环境

conda list命令用于查看conda下的包,而conda env list命令可以用来查看conda创建的所有虚拟环境。

下面就简述一下如何创建这些虚拟环境。
使用如下命令,可以创建一个新的环境:
conda create -n Python27 python=2.7
其中Python27是自定义的一个名称,而python=2.7是一个格式,可以变动等号右边的数字来改变python环境的kernel版本,这里我们安装的是python2.7版本(将于2020年停止维护)。
在anaconda prompt中,我们可以看到我们处在的是base环境下,也就是我安装的python3环境下,我们可以使用下面两个命令来切换环境:

注意:如果直接在ubuntu的命令行中切换环境,需要加上conda,比如conda activate 环境名

在创建环境的过程中,难免会不小心取了个难听的环境名,别担心,我们有方法来删除环境。
conda remove -n 难听的名字 --all
有时候一个环境已经配置好了,但我们想要重命名,这怎么办呢?可以这样办:

1
2
conda create -n 新名字 --clone 老名字
conda remove -n 老名字 --all

把环境添加到jupyter notebook

首先通过activate进入想要添加的环境中,然后安装ipykernel,接下来就可以进行添加了。

1
2
pip install ipykernel
python -m ipykernel install --name Python27 #Python27可以取与环境名不一样的名字,但方便起见建议统一

我们可以使用jupyter kernelspec list来查看已添加到jupyter notebook的kernel。
显示如下:

我们也可以在jupyter notebook中的new或者kernel下查看新环境是否成功添加。
若想删除某个指定的kernel,可以使用命令jupyter kernelspec remove kernel_name来完成。
在这里我想说明一下为什么要分开python的环境。
由于python是不向后兼容的,分开环境可以避免语法版本不一引起的错误,同时这也可以避免工具包安装与调用的混乱。


碰到底线咯 后面没有啦

本文标题:anaconda笔记:conda的各种命令行操作

文章作者:高深远

发布时间:2019年09月13日 - 23:17

最后更新:2020年02月19日 - 17:28

原始链接:https://gsy00517.github.io/anaconda20190913231748/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
anaconda笔记:解决conda无法下载pytorch的问题 | 高深远的博客

anaconda笔记:解决conda无法下载pytorch的问题

今天想加一个pytorch的新环境,无奈墙太厚,家里的网根本下载不下来,torch和torchvision两个包的进度始终只有0,网速下行也一直是0。在修改成清华镜像(目前国内唯一能用的,别的要么无法访问要么重定向)之后,还是没能成功。后来在网上找到了一种直接编译的方法,当然最后我没用,在理解了conda的命令操作后,我终于解决了这个问题。


解决方法

由于在安装pytorch的时候,我直接到官网里配置之后copy下来的,其命令如下:

1
conda install pytorch torchvision cpuonly -c pytorch

后来我才发现,最后的-c pytorch的作用是指定pytorch源作为channel来下载,这里指定的优先级比~/.condarc里设置的镜像源要高。
于是我去掉指定,调整如下:

1
conda install pytorch torchvision cpuonly

于是很快就用清华镜像下载好了。我这里下载的是无cuda版本,其他的一样。


添加清华源的方法

1
2
3
4
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --set show_channel_urls yes

或者直接vim ~/.condarc编辑,按i修改内容,再按ESC退出insert模式回到normal模式,最后使用:wq保存并退出。

注意:可以把~/.condarc里的- default删去。


碰到底线咯 后面没有啦

本文标题:anaconda笔记:解决conda无法下载pytorch的问题

文章作者:高深远

发布时间:2020年02月22日 - 00:00

最后更新:2020年02月26日 - 10:46

原始链接:https://gsy00517.github.io/anaconda20200222000018/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
anaconda笔记:解决conda无法下载pytorch的问题 | 高深远的博客

anaconda笔记:解决conda无法下载pytorch的问题

今天想加一个pytorch的新环境,无奈墙太厚,家里的网根本下载不下来,torch和torchvision两个包的进度始终只有0,网速下行也一直是0。在修改成清华镜像(目前国内唯一能用的,别的要么无法访问要么重定向)之后,还是没能成功。后来在网上找到了一种直接编译的方法,当然最后我没用,在理解了conda的命令操作后,我终于解决了这个问题。


解决方法

由于在安装pytorch的时候,我直接到官网里配置之后copy下来的,其命令如下:

1
conda install pytorch torchvision cpuonly -c pytorch

后来我才发现,最后的-c pytorch的作用是指定pytorch源作为channel来下载,这里指定的优先级比~/.condarc里设置的镜像源要高。
于是我去掉指定,调整如下:

1
conda install pytorch torchvision cpuonly

于是很快就用清华镜像下载好了。我这里下载的是无cuda版本,其他的一样。


添加清华源的方法

1
2
3
4
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --set show_channel_urls yes

或者直接vim ~/.condarc编辑,按i修改内容,再按ESC退出insert模式回到normal模式,最后使用:wq保存并退出。

注意:可以把~/.condarc里的- default删去。


碰到底线咯 后面没有啦

本文标题:anaconda笔记:解决conda无法下载pytorch的问题

文章作者:高深远

发布时间:2020年02月22日 - 00:00

最后更新:2020年02月26日 - 10:46

原始链接:https://gsy00517.github.io/anaconda20200222000018/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客归档 | 高深远的博客artificial intelligence笔记:人工智能前沿发展情况分享 | 高深远的博客

artificial intelligence笔记:人工智能前沿发展情况分享

这是我在一个相关的群里看到的一个论文,这篇论文比较新,看完之后觉得对目前AI发展状况的了解有一定价值,就放了上来。


论文

这里直接提供图片形式的原文:


其他

难得有一篇以AI开篇的文章,由于在我不到一年前真正接触AI相关知识时,一直疑惑人工智能、机器学习与深度学习之间的关系。直到看了台大教授李宏毅的课才知道三者之间的包含关系,这里就把课件中的一张图片放上来,一目了然:

最后,再补张和deep-learning笔记:一篇非常经典的论文——NatureDeepReview文末对应的一张我觉得挺真实的图哈哈。

不得不说,目前丰富的库和各种深度学习框架的确极大地方便了AI的学习与研究,许多轮子都已造好。学会运用这些工具还是很有帮助的!


碰到底线咯 后面没有啦

本文标题:artificial intelligence笔记:人工智能前沿发展情况分享

文章作者:高深远

发布时间:2019年10月01日 - 10:13

最后更新:2020年02月16日 - 07:14

原始链接:https://gsy00517.github.io/artificial-intelligence20191001101334/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
artificial intelligence笔记:人工智能前沿发展情况分享 | 高深远的博客

artificial intelligence笔记:人工智能前沿发展情况分享

这是我在一个相关的群里看到的一个论文,这篇论文比较新,看完之后觉得对目前AI发展状况的了解有一定价值,就放了上来。


论文

这里直接提供图片形式的原文:


其他

难得有一篇以AI开篇的文章,由于在我不到一年前真正接触AI相关知识时,一直疑惑人工智能、机器学习与深度学习之间的关系。直到看了台大教授李宏毅的课才知道三者之间的包含关系,这里就把课件中的一张图片放上来,一目了然:

最后,再补张和deep-learning笔记:一篇非常经典的论文——NatureDeepReview文末对应的一张我觉得挺真实的图哈哈。

不得不说,目前丰富的库和各种深度学习框架的确极大地方便了AI的学习与研究,许多轮子都已造好。学会运用这些工具还是很有帮助的!


碰到底线咯 后面没有啦

本文标题:artificial intelligence笔记:人工智能前沿发展情况分享

文章作者:高深远

发布时间:2019年10月01日 - 10:13

最后更新:2020年02月16日 - 07:14

原始链接:https://gsy00517.github.io/artificial-intelligence20191001101334/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
artificial intelligence笔记:阅读论文的建议 | 高深远的博客

artificial intelligence笔记:阅读论文的建议

接触科研,读paper是一件很头疼的事情。从当初用翻译软件两天一篇,到现在平均可以一日七八篇(放假时),我自己也有不小的感悟和进步。本文就来写一下吴恩达对于阅读ML、DL相关方面论文的建议和自己的一些感想,方便参考。


建议

首先要说明的是,这里的建议不是我想出来的,仅仅是对吴恩达提供的建议做搬运及整理。
如果你读到这里,应该也知道这个领域的先驱+巨佬Andrew Ng的大名。吴恩达(Andrew Ng),著名的美籍华裔计算机科学家,曾担任百度首席科学家,任教于Stanford,大家刚入门的时候想必都了解过或者看过由吴恩达老师讲授的斯坦福的经典课程CS229机器学习、CS230深度学习,此外,Andrew Ng还特地在网易云上为中国学生提供了中文字幕的课程(Andrew Ng英语说得比中文溜好多了哈哈)。另外,他还是著名教育平台Coursera的创始人,那里的课程更新鲜更优质,而且还可以锻炼英语能力,旁听即可。

呃放错了,不是上面那张,是这张。

对于如何阅读论文,Andrew Ng的建议是:
不要从头读到尾。相反,需要多次遍历论文。
具体有如下几个注意点:

  1. 阅读文章标题、摘要和图

    通过阅读文章标题、摘要、关键网络架构图,或许还有实验部分,你将能够对论文的概念有一个大致的了解。在深度学习中,有很多研究论文都是将整篇论文总结成一两个图形,而不需要费力地通读全文。尤其是在描述网络架构的时候,作者一般会采用比较通用的格式,读多了就会熟悉起来,比如下面DenseNet的结构:
  2. 读介绍、结论、图,略过其他

    介绍、结论和摘要是作者试图仔细总结自己工作的地方,以便向审稿人阐明为什么他们的论文应该被接受发表。
    此外,略过相关的工作部分(如果可能的话),这部分的目的是突出其他人所做的工作,这些工作在某种程度上与作者的工作有关。因此,阅读它可能是有用的,但如果不熟悉这个主题,有时会很难理解。
  3. 通读全文,但跳过数学部分

    这里我说一下我对于数学部分的处理:一般我会把重要的公式等略读一遍,然后参照着CSDN博客等网站上其他网友的解释与详解进行理解。
  4. 通读全文,但略过没有意义的部分

    Andrew Ng还解释说,当你阅读论文时(即使是最有影响力的论文),你可能也会发现有些部分没什么用,或者没什么意义。因此,如果你读了一篇论文,其中一些内容没有意义(这并不罕见),那么你可以先略读。除非你想要掌握它,那就花更多的时间。确实,当我在阅读ILSVRC、COCO等顶级比赛许多获奖模型的论文时,其中都有对比赛情况的详细结果介绍,我觉得这些部分一定程度上是可以扫读和跳读的。

感悟

目前我阅读过大多数论文的组成一般为Abstract、Introduction、Related Work、Method、Experiment、Conclusion。
可能阅历丰富的研究者能够从Abstract中提炼出比较关键的信息,但我个人认为,一篇文章最关键的是Introduction部分,该部分一般会包括前因后果和文章有哪几个contribution的总结,涵盖了本文解决的问题和一些主要概念和思路。
Related Work主要是对之前论文的分析,相当于是Introduction部分中前因的具体分析,相当于精简版的综述,如果对该领域比较了解的话完全可以跳读,但如果不熟悉该领域的话还是能从中学到很多重要的信息的。
Method部分是对Introduction部分中contribution的具体化,从这里开始会涉及到较多理论的内容,细品还是略读要靠自己拿捏。
Experiment是比较容易忽略的部分,不过有时候会提及一些trick,如果有ablation experiments的话可以重点关注一下。注意,在一些预印论文网站上的占坑论文有可能实验部分不是很完全,这是我们用于判断该篇论文结论是否可靠的依据。
Conclusion其实是Introduction的缩写,如果有未来研究方向的展望的话可以看一下。


分享

关于论文,我之前也做过一些分享,详情可以看看我之前的文章。
deep-learning笔记:开启深度学习热潮——AlexNet一文中,我提到了刚开始阅读英文论文的比较有效的方法。
deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现一文中,我也提供了许多经典模型论文的英文版、中文版、中英对照的链接。
最后要说明的是,本篇文章中Andrew Ng的建议有部分摘自公众号Datawhale的推送文章。我关注了不少这方面的公众号,筛选了几个比较优质的,在今后也会一一放到博客中推荐。


碰到底线咯 后面没有啦

本文标题:artificial intelligence笔记:阅读论文的建议

文章作者:高深远

发布时间:2019年10月07日 - 23:25

最后更新:2020年03月13日 - 16:21

原始链接:https://gsy00517.github.io/artificial-intelligence20191007232512/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
artificial intelligence笔记:阅读论文的建议 | 高深远的博客

artificial intelligence笔记:阅读论文的建议

接触科研,读paper是一件很头疼的事情。从当初用翻译软件两天一篇,到现在平均可以一日七八篇(放假时),我自己也有不小的感悟和进步。本文就来写一下吴恩达对于阅读ML、DL相关方面论文的建议和自己的一些感想,方便参考。


建议

首先要说明的是,这里的建议不是我想出来的,仅仅是对吴恩达提供的建议做搬运及整理。
如果你读到这里,应该也知道这个领域的先驱+巨佬Andrew Ng的大名。吴恩达(Andrew Ng),著名的美籍华裔计算机科学家,曾担任百度首席科学家,任教于Stanford,大家刚入门的时候想必都了解过或者看过由吴恩达老师讲授的斯坦福的经典课程CS229机器学习、CS230深度学习,此外,Andrew Ng还特地在网易云上为中国学生提供了中文字幕的课程(Andrew Ng英语说得比中文溜好多了哈哈)。另外,他还是著名教育平台Coursera的创始人,那里的课程更新鲜更优质,而且还可以锻炼英语能力,旁听即可。

呃放错了,不是上面那张,是这张。

对于如何阅读论文,Andrew Ng的建议是:
不要从头读到尾。相反,需要多次遍历论文。
具体有如下几个注意点:

  1. 阅读文章标题、摘要和图

    通过阅读文章标题、摘要、关键网络架构图,或许还有实验部分,你将能够对论文的概念有一个大致的了解。在深度学习中,有很多研究论文都是将整篇论文总结成一两个图形,而不需要费力地通读全文。尤其是在描述网络架构的时候,作者一般会采用比较通用的格式,读多了就会熟悉起来,比如下面DenseNet的结构:
  2. 读介绍、结论、图,略过其他

    介绍、结论和摘要是作者试图仔细总结自己工作的地方,以便向审稿人阐明为什么他们的论文应该被接受发表。
    此外,略过相关的工作部分(如果可能的话),这部分的目的是突出其他人所做的工作,这些工作在某种程度上与作者的工作有关。因此,阅读它可能是有用的,但如果不熟悉这个主题,有时会很难理解。
  3. 通读全文,但跳过数学部分

    这里我说一下我对于数学部分的处理:一般我会把重要的公式等略读一遍,然后参照着CSDN博客等网站上其他网友的解释与详解进行理解。
  4. 通读全文,但略过没有意义的部分

    Andrew Ng还解释说,当你阅读论文时(即使是最有影响力的论文),你可能也会发现有些部分没什么用,或者没什么意义。因此,如果你读了一篇论文,其中一些内容没有意义(这并不罕见),那么你可以先略读。除非你想要掌握它,那就花更多的时间。确实,当我在阅读ILSVRC、COCO等顶级比赛许多获奖模型的论文时,其中都有对比赛情况的详细结果介绍,我觉得这些部分一定程度上是可以扫读和跳读的。

感悟

目前我阅读过大多数论文的组成一般为Abstract、Introduction、Related Work、Method、Experiment、Conclusion。
可能阅历丰富的研究者能够从Abstract中提炼出比较关键的信息,但我个人认为,一篇文章最关键的是Introduction部分,该部分一般会包括前因后果和文章有哪几个contribution的总结,涵盖了本文解决的问题和一些主要概念和思路。
Related Work主要是对之前论文的分析,相当于是Introduction部分中前因的具体分析,相当于精简版的综述,如果对该领域比较了解的话完全可以跳读,但如果不熟悉该领域的话还是能从中学到很多重要的信息的。
Method部分是对Introduction部分中contribution的具体化,从这里开始会涉及到较多理论的内容,细品还是略读要靠自己拿捏。
Experiment是比较容易忽略的部分,不过有时候会提及一些trick,如果有ablation experiments的话可以重点关注一下。注意,在一些预印论文网站上的占坑论文有可能实验部分不是很完全,这是我们用于判断该篇论文结论是否可靠的依据。
Conclusion其实是Introduction的缩写,如果有未来研究方向的展望的话可以看一下。


分享

关于论文,我之前也做过一些分享,详情可以看看我之前的文章。
deep-learning笔记:开启深度学习热潮——AlexNet一文中,我提到了刚开始阅读英文论文的比较有效的方法。
deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现一文中,我也提供了许多经典模型论文的英文版、中文版、中英对照的链接。
最后要说明的是,本篇文章中Andrew Ng的建议有部分摘自公众号Datawhale的推送文章。我关注了不少这方面的公众号,筛选了几个比较优质的,在今后也会一一放到博客中推荐。


碰到底线咯 后面没有啦

本文标题:artificial intelligence笔记:阅读论文的建议

文章作者:高深远

发布时间:2019年10月07日 - 23:25

最后更新:2020年03月13日 - 16:21

原始链接:https://gsy00517.github.io/artificial-intelligence20191007232512/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
]]> +

前段时间在服务器上使用matlab,在激活的时候遇到了一些和自己个人电脑上不一样的情况,在搞清楚之后记录一下。

References

电子文献:
https://ww2.mathworks.cn/matlabcentral/answers/99067-why-do-i-receive-license-manager-error-9


问题

前段时间在学校的ubuntu服务器上安装了matlab,按照自己之前的方法在/usr/local/MATLAB/version/bin中使用./activate_matlab.sh激活并使用matlab命令直接运行,却卡在MATLAB is selecting SOFTWARE OPENGL rendering之后报错License Manager Error -9
我们知道,matlab激活时指定的用户是需要和本机用户名一致的,由于之前服务器上有学姐安装过matlab,我觉得可能是激活了她的用户名导致我无法使用,因此我反激活之前的用户名之后,尝试使用root作为指定用户重新激活并使用sudo matlab命令运行matlab,结果却显示没有已激活的许可证。
后来想想的确不应该是这个问题,因为matlab每次激活会覆盖之前的,假如是因为别的用户名导致我无法运行的话,我激活后应该就能解决,然而我却依旧不能运行。
于是,我直接在bin中打开matlab的执行文件,即./matlab(如果之前用root激活的话则输入sudo ./matlab就能执行),打开是打开了,但打开过程比较慢且出现一长串报错。


解决办法

在搞清楚matlab激活的要求之后,我还是使用自己的用户名去激活matlab。当然,别的用户也可以使用自己的用户名激活,经检验,这样并不会影响大家共同的使用。
然而,此时仍旧只能在bin中打开matlab并且会有如上文图中所示的报错。搜索之后发现,是我没有把.matlab的整个目录设为可写。注意,这个目录指的是用户主文件夹下的目录而不是/usr/local下面的安装目录。
因此,只需赋予更多权限,就不会出现之前的问题了。可以在终端中执行如下语句。

1
sudo chmod -R 777 /home/***/.matlab

注意:这里的***是用户名,-R是为了对整个文件夹操作。


如何查看本机用户名

一般情况下,执行matlab激活程序时会自动给出你的用户名,这样激活之后就可以直接使用。然而,有时候用户名并没有给出而需要自己填,而有可能不确定自己该填啥,这时候可以就通过如下方式查询。

  1. 还是执行激活程序,先不选择使用Internet自动激活,而是选择“在不使用Internet的情况下手动激活”。
  2. 点击下一步之后,选择“我没有许可证文件,帮我执行后续步骤”。
  3. 再到下一步,就可以看到用户名了,直接复制。
  4. 返回到开始的地方,选择“使用Internet自动激活”,之后还是一路默认操作,在需要指定用户时把之前的用户名粘贴上即可。
]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>前段时间在服务器上使用matlab,在激活的时候遇到了一些和自己个人电脑上不一样的情况,在搞清楚之后记录一下。</p><p><strong>Re + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>前段时间在服务器上使用matlab,在激活的时候遇到了一些和自己个人电脑上不一样的情况,在搞清楚之后记录一下。</p><p><strong>Re @@ -51,13 +51,13 @@ 2020-03-12T09:22:47.000Z 2020-03-12T10:10:05.971Z -

这篇文章要从一只蝙蝠说起…好吧其实这是一篇没有任何技术含量的文章。纯粹心血来潮,研究了一下路由器的天线(我家是三根)的摆放方法与网速之间的关系。


起因

不知道怎么,总感觉家里“专属”的WiFi信号反而没流量快。上网一查,才发现我们在商场或者网上看到的路由器商品展示的天线摆放方法(全部竖直向上)是错误的。
根据网上资料,一般家用路由器的全向天线的无线信号分布类似被压扁的椭球形,无线信号在与天线垂直的方向上覆盖效果最好。由于手机的天线位于手机边框内、笔记本电脑的天线位于连接屏幕的水平转轴中…而设备的不同摆放方式也会导致接收信号的效果不同。后面我会证明天线朝同一个方向的叠加摆放与信号的增强没有必然联系,因此正确的摆法应该交叉而不指向同一方向。若是两根天线,则应成直角;若三根,则应如下图所示。


实验结果

为了取得比较可靠的数据,我借鉴了物理实验中交替重复测量的思想,分别进行了不同向、同向、再不同向、再同向…做了共8次实验,结果如下(仅展示其中4次)。




可以看到,两种摆法的延迟和下行速度并没有很大区别(全部竖直向上摆放下行稍快),而上行速度却有显著区别。可见天线朝同一个方向的叠加摆放与信号的增强没有必然联系(有说法说反而会干扰),而朝不同的方向确实利于接收信号,提高上行速度。不过一般我们使用网络大部分都是下行传递数据,总的来说,两种方法各有细微的利弊但在使用上区别不大。

]]>
+

这篇文章要从一只蝙蝠说起…好吧其实这是一篇没有任何技术含量的文章。纯粹心血来潮,研究了一下路由器的天线(我家是三根)的摆放方法与网速之间的关系。


起因

不知道怎么,总感觉家里“专属”的WiFi信号反而没流量快。上网一查,才发现我们在商场或者网上看到的路由器商品展示的天线摆放方法(全部竖直向上)是错误的。
根据网上资料,一般家用路由器的全向天线的无线信号分布类似被压扁的椭球形,无线信号在与天线垂直的方向上覆盖效果最好。由于手机的天线位于手机边框内、笔记本电脑的天线位于连接屏幕的水平转轴中…而设备的不同摆放方式也会导致接收信号的效果不同。后面我会证明天线朝同一个方向的叠加摆放与信号的增强没有必然联系,因此正确的摆法应该交叉而不指向同一方向。若是两根天线,则应成直角;若三根,则应如下图所示。


实验结果

为了取得比较可靠的数据,我借鉴了物理实验中交替重复测量的思想,分别进行了不同向、同向、再不同向、再同向…做了共8次实验,结果如下(仅展示其中4次)。




可以看到,两种摆法的延迟和下行速度并没有很大区别(全部竖直向上摆放下行稍快),而上行速度却有显著区别。可见天线朝同一个方向的叠加摆放与信号的增强没有必然联系(有说法说反而会干扰),而朝不同的方向确实利于接收信号,提高上行速度。不过一般我们使用网络大部分都是下行传递数据,总的来说,两种方法各有细微的利弊但在使用上区别不大。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>这篇文章要从一只蝙蝠说起…好吧其实这是一篇没有任何技术含量的文章。纯粹心血来潮,研究了一下路由器的天线(我家是三根)的摆放方法与网速之间的关系。 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>这篇文章要从一只蝙蝠说起…好吧其实这是一篇没有任何技术含量的文章。纯粹心血来潮,研究了一下路由器的天线(我家是三根)的摆放方法与网速之间的关系。 @@ -77,13 +77,13 @@ 2020-03-12T08:35:04.000Z 2020-03-12T08:44:07.374Z -

今天我购买的VPN订阅服务的服务器由于技术人员的误操作导致系统被重建,好在可靠的商家留存了备份并且在第一时间重建了。然而尽管商家那边的主域名一定DNS生效了,我本地依旧无法成功通过对应域名成功登录。于是我找到了下面这种立即刷新DNS缓存的方法。


DNS缓存

为了提高网站的访问速度,系统会在成功访问某网站后将该网站的域名、IP地址信息缓存到本地。这样在下次访问该域名时,可以直接通过IP进行访问。可是,如果一些网站的域名没有变化但IP地址发生变化(比如上述情况),就有可能因本地的DNS缓存没有刷新导致无法访问。


立即刷新方法

在windows下,win+R输入cmd打开命令行,输入ipconfig /flushdns并执行,此时就完成了对DNS缓存的立即刷新。

]]>
+

今天我购买的VPN订阅服务的服务器由于技术人员的误操作导致系统被重建,好在可靠的商家留存了备份并且在第一时间重建了。然而尽管商家那边的主域名一定DNS生效了,我本地依旧无法成功通过对应域名成功登录。于是我找到了下面这种立即刷新DNS缓存的方法。


DNS缓存

为了提高网站的访问速度,系统会在成功访问某网站后将该网站的域名、IP地址信息缓存到本地。这样在下次访问该域名时,可以直接通过IP进行访问。可是,如果一些网站的域名没有变化但IP地址发生变化(比如上述情况),就有可能因本地的DNS缓存没有刷新导致无法访问。


立即刷新方法

在windows下,win+R输入cmd打开命令行,输入ipconfig /flushdns并执行,此时就完成了对DNS缓存的立即刷新。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>今天我购买的VPN订阅服务的服务器由于技术人员的误操作导致系统被重建,好在可靠的商家留存了备份并且在第一时间重建了。然而尽管商家那边的主域名一定 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>今天我购买的VPN订阅服务的服务器由于技术人员的误操作导致系统被重建,好在可靠的商家留存了备份并且在第一时间重建了。然而尽管商家那边的主域名一定 @@ -105,13 +105,13 @@ 2020-02-28T03:14:30.000Z 2020-02-29T05:14:59.909Z -

在pytorch的张量计算中,“广播”指的是当满足一定条件时,较小的张量能够自动扩张成合适尺寸的大张量,使得能够进行计算。

References

电子文献:
https://pytorch.org/docs/stable/notes/broadcasting.html

条件

当一对张量满足下面的条件时,它们才是可以被“广播”的。

  1. 每个张量至少有一个维度。
  2. 迭代维度尺寸时,从尾部(也就是从后往前)开始,依次每个维度的尺寸必须满足以下之一:
    • 相等
    • 其中一个张量的维度尺寸为1
    • 其中一个张量不存在这个维度。

例子

光看条件可能会有点迷,下面是官方文档中的几个例子。

1
import torch

首先,显然相同形状的张量是可以广播的(或者说不需要广播)。

1
2
x = torch.empty(5, 7, 3)
y = torch.empty(5, 7, 3)

(x+y).size()输出torch.Size([5, 7, 3])
一般而言,我们可以从后往前分析一对张量是不是“broadcastable”。

1
2
x = torch.empty(5, 3, 4, 1)
y = torch.empty(3, 1, 1)

我们依次分析:
对倒数第一个维度,两者尺寸相同,符合“相等”的条件。
对倒数第二个维度,y的尺寸为1,符合“其中一个张量尺寸为1”的条件。
对倒数第三个维度,两者尺寸相同,符合“相等”的条件。
对倒数第四个维度,y的该维度不存在,符合“其中一个张量不存在这个维度”的条件。
综上,这两个张量可以广播。
其实,x可以比y多出更多的维度,都满足该维度有“其中一个张量不存在这个维度”的条件。
举个反例:

1
2
x = torch.empty(5, 2, 4, 1)
y = torch.empty(3, 1, 1)

这里若x+y就无法广播了,因为从后往前遇到倒数第三个维度时会被卡住,不满足任何一个条件。
此外,对torch.empty((0,)),它也无法和其他任何张量广播,可以输出发现其结果为tensor([]),是空的,这就不满足“每个张量至少有一个维度”这个条件。


tensor.view()

这里做个补充,一般我们在全连接层,总是会看到对某个张量作view(-1, ...)的变换,其实这里的“-1”并不是直接表示尺寸,而是起到让计算机帮助自动计算的功能。
举个例子,比如一个tensor含有6个数据(无论唯独尺寸如何组合),我们对其view(-1, 2),那么输出的尺寸就是[3, 2],若view(-1),那么输出的尺寸就是[6]。
注意,不能同时出现两个“-1”让机器计算。虽然在某些情况下,人能够判断,比如含有6个数据的tensor我们能够想到view(-1, -1, 6)输出尺寸是[1, 1, 6],但机器就会认为这是个错误。

]]>
+

在pytorch的张量计算中,“广播”指的是当满足一定条件时,较小的张量能够自动扩张成合适尺寸的大张量,使得能够进行计算。

References

电子文献:
https://pytorch.org/docs/stable/notes/broadcasting.html

条件

当一对张量满足下面的条件时,它们才是可以被“广播”的。

  1. 每个张量至少有一个维度。
  2. 迭代维度尺寸时,从尾部(也就是从后往前)开始,依次每个维度的尺寸必须满足以下之一:
    • 相等
    • 其中一个张量的维度尺寸为1
    • 其中一个张量不存在这个维度。

例子

光看条件可能会有点迷,下面是官方文档中的几个例子。

1
import torch

首先,显然相同形状的张量是可以广播的(或者说不需要广播)。

1
2
x = torch.empty(5, 7, 3)
y = torch.empty(5, 7, 3)

(x+y).size()输出torch.Size([5, 7, 3])
一般而言,我们可以从后往前分析一对张量是不是“broadcastable”。

1
2
x = torch.empty(5, 3, 4, 1)
y = torch.empty(3, 1, 1)

我们依次分析:
对倒数第一个维度,两者尺寸相同,符合“相等”的条件。
对倒数第二个维度,y的尺寸为1,符合“其中一个张量尺寸为1”的条件。
对倒数第三个维度,两者尺寸相同,符合“相等”的条件。
对倒数第四个维度,y的该维度不存在,符合“其中一个张量不存在这个维度”的条件。
综上,这两个张量可以广播。
其实,x可以比y多出更多的维度,都满足该维度有“其中一个张量不存在这个维度”的条件。
举个反例:

1
2
x = torch.empty(5, 2, 4, 1)
y = torch.empty(3, 1, 1)

这里若x+y就无法广播了,因为从后往前遇到倒数第三个维度时会被卡住,不满足任何一个条件。
此外,对torch.empty((0,)),它也无法和其他任何张量广播,可以输出发现其结果为tensor([]),是空的,这就不满足“每个张量至少有一个维度”这个条件。


tensor.view()

这里做个补充,一般我们在全连接层,总是会看到对某个张量作view(-1, ...)的变换,其实这里的“-1”并不是直接表示尺寸,而是起到让计算机帮助自动计算的功能。
举个例子,比如一个tensor含有6个数据(无论唯独尺寸如何组合),我们对其view(-1, 2),那么输出的尺寸就是[3, 2],若view(-1),那么输出的尺寸就是[6]。
注意,不能同时出现两个“-1”让机器计算。虽然在某些情况下,人能够判断,比如含有6个数据的tensor我们能够想到view(-1, -1, 6)输出尺寸是[1, 1, 6],但机器就会认为这是个错误。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>在pytorch的张量计算中,“广播”指的是当满足一定条件时,较小的张量能够自动扩张成合适尺寸的大张量,使得能够进行计算。</p><p><str + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在pytorch的张量计算中,“广播”指的是当满足一定条件时,较小的张量能够自动扩张成合适尺寸的大张量,使得能够进行计算。</p><p><str @@ -131,13 +131,13 @@ 2020-02-26T01:23:14.000Z 2020-02-26T03:04:15.640Z -

跑CV程序少不了opencv,然后最近实践的时候发现opencv似乎不是向后兼容的(opencv4.0跑不了opencv2.4的),于是还是记录一下,后期改也方便。

References

电子文献:
https://blog.csdn.net/new_delete_/article/details/84797041?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.jianshu.com/p/f646448da265
https://blog.csdn.net/learning_tortosie/article/details/80594399


安装流程

本节以opencv4.0为例,记录一下在ubuntu18.04上安装的流程。此方法经检验,同样适用于opencv2.4。

准备

cmake

首先要确保系统已经安装了cmake,没有的话请sudo apt-get install cmake或者源码编译最新版。

依赖库

1
sudo apt-get install build-essential libgtk2.0-dev libgtk-3-dev libavcodec-dev libavformat-dev libjpeg-dev libswscale-dev libtiff5-dev

支持python

1
2
3
4
5
6
7
8
#python3支持
sudo apt install python3-dev python3-numpy

#streamer支持
sudo apt install libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev

#可选的依赖
sudo apt install libpng-dev libopenexr-dev libtiff-dev libwebp-dev

下载源文件

可以到官网或者github上去下载源文件(官网版本选择在下方翻页,github上通过tag选择)。
推荐使用github下载,相对会快一些。

安装

解压源文件并进入。

1
2
unzip opencv-4.0.0.zip
cd opencv-4.0.0/

创建编译文件夹并进入。

1
2
mkdir build
cd build/

cmake

opencv最好装在/usr/local目录下,为了后期方便,我将不同版本的opencv再独立键一级目录。

1
cmake -D CMAKE_BUILD_TYPE=Release -D OPENCV_GENERATE_PKGCONFIG=YES -D CMAKE_INSTALL_PREFIX=/usr/local/opencv4 ..

这里我修改了安装路径为/usr/local/opencv4,可以自己修改到需要的位置,如果该命令中不加-D CMAKE_INSTALL_PREFIX=/usr/local/opencv4来指定安装目录,则默认各部分分别安装在/usr/local/目录的includebinlib3个文件夹下。

注意:
别忘了最后的..。(cmake .是运行cmake,cmake ..会生成Makefile、CMakeFiles等一些文件)
由于opencv4.0以上版本默认不使用pkg-config,因此需要通过-D OPENCV_GENERATE_PKGCONFIG=YES开启生成opencv4.pc文件(后面要用),支持pkg-config功能。如果是安装opencv4.0以下的软件,也加上这句是不会有影响的。
这个命令执行需要一段时间,期间会输出百分比,无需担心。

make编译

1
make -j6

这里-j6表示开6个线程去编译,-j2-j4或者不加其实任意。

make安装

1
sudo make install

以上就完成了安装,下面来配置环境。

配置环境

pkg-config环境

我们可以通过sudo find / -iname opencv4.pc来找到opencv4.pc文件,这里会出现一个Permission denied的报错,但是没关系,路径还是会输出。如果按照之前的方法安装,那么应该是会输出/usr/local/opencv4/lib/pkgconfig/opencv4.pc。因此我们接下来就要把路径/usr/local/opencv4/lib/pkgconfig/加入到PKG_CONFIG_PATH
可以用vim来方便地编辑。编辑方法我在anaconda笔记:解决conda无法下载pytorch的问题一文中已经提过,只需掌握insert模式和normal模式即可正确地操作vim。

1
sudo vim /etc/profile.d/pkgconfig.sh

在文件中加入下面一行:

1
export PKG_CONFIG_PATH=/usr/local/opencv4/lib/pkgconfig:$PKG_CONFIG_PATH

:wq保存退出之后,在终端中使用命令激活。

1
source /etc/profile

最后我们可以通过pkg-config --libs opencv4(opencv2.4使用pkg-config --libs opencv)来验证配置是否成功,如果能输出一系列对应的库,说明配置成功。

动态库环境

为了在程序执行时能加载动态库*.so的路径,我们还需配置动态库环境。

1
sudo vim /etc/ld.so.conf.d/opencv4.conf

在该文件(可能是空文件,也可能是/usr/local/lib),加上或者改成自己lib的安装路径,如果是按照上面的流程安装的,那应该是改成/usr/local/opencv4/lib
最后还要用命令使得配置生效。

1
sudo ldconfig

python-opencv环境

找到编译好的python cv库:

1
sudo find / -iname cv2*.so

这里会看到cv2.cpython-35m-x86_64-linux-gnu.so也就是就是编译好的python3的opencv库,我们把它复制到对应python解释器的/path/to/dist-packages(系统自带的python解释器)和/path/to/site-packages(用户安装的python解释器)目录下,之后就能在该python解释器中使用python-opencv库。
连接到系统自带的python3解释器中:

1
sudo ln -s /usr/local/opencv4/lib/python3.5/dist-packages/cv2/python-3.5/cv2.cpython-35m-x86_64-linux-gnu.so /usr/lib/python3/dist-packages/cv2.so

连接到Anaconda创建的虚拟环境python3解释器中:

1
sudo ln -s /usr/local/opencv4/lib/python3.5/dist-packages/cv2/python-3.5/cv2.cpython-35m-x86_64-linux-gnu.so ~/anaconda3/lib/python3.7/site-packages/cv2.so

这里的ln -s是建立软连接,想进一步了解可以看看ubuntu笔记:安装typora

检验

找到之前解压的源文件,在源文件/samples/cpp/example_cmake目录下(也可能是在源文件/samples/c/example_cmake目录下),我们可以通过官方提供的example来检验。
依次执行:

1
2
3
cmake .
make
./opencv_example

可以看到电脑打开了摄像头拍你自己,在左上角有一个Hello OpenCV,即表示配置成功。


多版本共存

上面已经安装了opencv4.0,下面以opencv2.4为例,介绍一下如何从opencv4.0切换到opencv2.4。
打开~/.bashrc

1
gedit ~/.bashrc

这里用gedit来打开,用vim也一样。
在文件末尾增加如下内容(我将opencv2.4安装在/usr/local/opencv2_4目录下):

1
2
export PKG_CONFIG_PATH=/usr/local/opencv2_4/lib/pkgconfig
export LD_LIBRARY_PATH=/usr/local/opencv2_4/lib

更新~/.bashrc

1
source ~/.bashrc

此时再通过pkg-config --modversion opencv查询opencv的版本,如输出修改后我们想要的版本,则表示切换成功。

注:如果想换回来,用同样的方法把之前加的两条增加的内容注释掉即可,注意别忘了source ~/.bashrc更新!

]]>
+

跑CV程序少不了opencv,然后最近实践的时候发现opencv似乎不是向后兼容的(opencv4.0跑不了opencv2.4的),于是还是记录一下,后期改也方便。

References

电子文献:
https://blog.csdn.net/new_delete_/article/details/84797041?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.jianshu.com/p/f646448da265
https://blog.csdn.net/learning_tortosie/article/details/80594399


安装流程

本节以opencv4.0为例,记录一下在ubuntu18.04上安装的流程。此方法经检验,同样适用于opencv2.4。

准备

cmake

首先要确保系统已经安装了cmake,没有的话请sudo apt-get install cmake或者源码编译最新版。

依赖库

1
sudo apt-get install build-essential libgtk2.0-dev libgtk-3-dev libavcodec-dev libavformat-dev libjpeg-dev libswscale-dev libtiff5-dev

支持python

1
2
3
4
5
6
7
8
#python3支持
sudo apt install python3-dev python3-numpy

#streamer支持
sudo apt install libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev

#可选的依赖
sudo apt install libpng-dev libopenexr-dev libtiff-dev libwebp-dev

下载源文件

可以到官网或者github上去下载源文件(官网版本选择在下方翻页,github上通过tag选择)。
推荐使用github下载,相对会快一些。

安装

解压源文件并进入。

1
2
unzip opencv-4.0.0.zip
cd opencv-4.0.0/

创建编译文件夹并进入。

1
2
mkdir build
cd build/

cmake

opencv最好装在/usr/local目录下,为了后期方便,我将不同版本的opencv再独立键一级目录。

1
cmake -D CMAKE_BUILD_TYPE=Release -D OPENCV_GENERATE_PKGCONFIG=YES -D CMAKE_INSTALL_PREFIX=/usr/local/opencv4 ..

这里我修改了安装路径为/usr/local/opencv4,可以自己修改到需要的位置,如果该命令中不加-D CMAKE_INSTALL_PREFIX=/usr/local/opencv4来指定安装目录,则默认各部分分别安装在/usr/local/目录的includebinlib3个文件夹下。

注意:
别忘了最后的..。(cmake .是运行cmake,cmake ..会生成Makefile、CMakeFiles等一些文件)
由于opencv4.0以上版本默认不使用pkg-config,因此需要通过-D OPENCV_GENERATE_PKGCONFIG=YES开启生成opencv4.pc文件(后面要用),支持pkg-config功能。如果是安装opencv4.0以下的软件,也加上这句是不会有影响的。
这个命令执行需要一段时间,期间会输出百分比,无需担心。

make编译

1
make -j6

这里-j6表示开6个线程去编译,-j2-j4或者不加其实任意。

make安装

1
sudo make install

以上就完成了安装,下面来配置环境。

配置环境

pkg-config环境

我们可以通过sudo find / -iname opencv4.pc来找到opencv4.pc文件,这里会出现一个Permission denied的报错,但是没关系,路径还是会输出。如果按照之前的方法安装,那么应该是会输出/usr/local/opencv4/lib/pkgconfig/opencv4.pc。因此我们接下来就要把路径/usr/local/opencv4/lib/pkgconfig/加入到PKG_CONFIG_PATH
可以用vim来方便地编辑。编辑方法我在anaconda笔记:解决conda无法下载pytorch的问题一文中已经提过,只需掌握insert模式和normal模式即可正确地操作vim。

1
sudo vim /etc/profile.d/pkgconfig.sh

在文件中加入下面一行:

1
export PKG_CONFIG_PATH=/usr/local/opencv4/lib/pkgconfig:$PKG_CONFIG_PATH

:wq保存退出之后,在终端中使用命令激活。

1
source /etc/profile

最后我们可以通过pkg-config --libs opencv4(opencv2.4使用pkg-config --libs opencv)来验证配置是否成功,如果能输出一系列对应的库,说明配置成功。

动态库环境

为了在程序执行时能加载动态库*.so的路径,我们还需配置动态库环境。

1
sudo vim /etc/ld.so.conf.d/opencv4.conf

在该文件(可能是空文件,也可能是/usr/local/lib),加上或者改成自己lib的安装路径,如果是按照上面的流程安装的,那应该是改成/usr/local/opencv4/lib
最后还要用命令使得配置生效。

1
sudo ldconfig

python-opencv环境

找到编译好的python cv库:

1
sudo find / -iname cv2*.so

这里会看到cv2.cpython-35m-x86_64-linux-gnu.so也就是就是编译好的python3的opencv库,我们把它复制到对应python解释器的/path/to/dist-packages(系统自带的python解释器)和/path/to/site-packages(用户安装的python解释器)目录下,之后就能在该python解释器中使用python-opencv库。
连接到系统自带的python3解释器中:

1
sudo ln -s /usr/local/opencv4/lib/python3.5/dist-packages/cv2/python-3.5/cv2.cpython-35m-x86_64-linux-gnu.so /usr/lib/python3/dist-packages/cv2.so

连接到Anaconda创建的虚拟环境python3解释器中:

1
sudo ln -s /usr/local/opencv4/lib/python3.5/dist-packages/cv2/python-3.5/cv2.cpython-35m-x86_64-linux-gnu.so ~/anaconda3/lib/python3.7/site-packages/cv2.so

这里的ln -s是建立软连接,想进一步了解可以看看ubuntu笔记:安装typora

检验

找到之前解压的源文件,在源文件/samples/cpp/example_cmake目录下(也可能是在源文件/samples/c/example_cmake目录下),我们可以通过官方提供的example来检验。
依次执行:

1
2
3
cmake .
make
./opencv_example

可以看到电脑打开了摄像头拍你自己,在左上角有一个Hello OpenCV,即表示配置成功。


多版本共存

上面已经安装了opencv4.0,下面以opencv2.4为例,介绍一下如何从opencv4.0切换到opencv2.4。
打开~/.bashrc

1
gedit ~/.bashrc

这里用gedit来打开,用vim也一样。
在文件末尾增加如下内容(我将opencv2.4安装在/usr/local/opencv2_4目录下):

1
2
export PKG_CONFIG_PATH=/usr/local/opencv2_4/lib/pkgconfig
export LD_LIBRARY_PATH=/usr/local/opencv2_4/lib

更新~/.bashrc

1
source ~/.bashrc

此时再通过pkg-config --modversion opencv查询opencv的版本,如输出修改后我们想要的版本,则表示切换成功。

注:如果想换回来,用同样的方法把之前加的两条增加的内容注释掉即可,注意别忘了source ~/.bashrc更新!

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>跑CV程序少不了opencv,然后最近实践的时候发现opencv似乎不是向后兼容的(opencv4.0跑不了opencv2.4的),于是还是记录 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>跑CV程序少不了opencv,然后最近实践的时候发现opencv似乎不是向后兼容的(opencv4.0跑不了opencv2.4的),于是还是记录 @@ -163,13 +163,13 @@ 2020-02-21T16:00:18.000Z 2020-02-26T02:46:24.546Z -

今天想加一个pytorch的新环境,无奈墙太厚,家里的网根本下载不下来,torch和torchvision两个包的进度始终只有0,网速下行也一直是0。在修改成清华镜像(目前国内唯一能用的,别的要么无法访问要么重定向)之后,还是没能成功。后来在网上找到了一种直接编译的方法,当然最后我没用,在理解了conda的命令操作后,我终于解决了这个问题。


解决方法

由于在安装pytorch的时候,我直接到官网里配置之后copy下来的,其命令如下:

1
conda install pytorch torchvision cpuonly -c pytorch

后来我才发现,最后的-c pytorch的作用是指定pytorch源作为channel来下载,这里指定的优先级比~/.condarc里设置的镜像源要高。
于是我去掉指定,调整如下:

1
conda install pytorch torchvision cpuonly

于是很快就用清华镜像下载好了。我这里下载的是无cuda版本,其他的一样。


添加清华源的方法

1
2
3
4
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --set show_channel_urls yes

或者直接vim ~/.condarc编辑,按i修改内容,再按ESC退出insert模式回到normal模式,最后使用:wq保存并退出。

注意:可以把~/.condarc里的- default删去。

]]>
+

今天想加一个pytorch的新环境,无奈墙太厚,家里的网根本下载不下来,torch和torchvision两个包的进度始终只有0,网速下行也一直是0。在修改成清华镜像(目前国内唯一能用的,别的要么无法访问要么重定向)之后,还是没能成功。后来在网上找到了一种直接编译的方法,当然最后我没用,在理解了conda的命令操作后,我终于解决了这个问题。


解决方法

由于在安装pytorch的时候,我直接到官网里配置之后copy下来的,其命令如下:

1
conda install pytorch torchvision cpuonly -c pytorch

后来我才发现,最后的-c pytorch的作用是指定pytorch源作为channel来下载,这里指定的优先级比~/.condarc里设置的镜像源要高。
于是我去掉指定,调整如下:

1
conda install pytorch torchvision cpuonly

于是很快就用清华镜像下载好了。我这里下载的是无cuda版本,其他的一样。


添加清华源的方法

1
2
3
4
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --set show_channel_urls yes

或者直接vim ~/.condarc编辑,按i修改内容,再按ESC退出insert模式回到normal模式,最后使用:wq保存并退出。

注意:可以把~/.condarc里的- default删去。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>今天想加一个pytorch的新环境,无奈墙太厚,家里的网根本下载不下来,torch和torchvision两个包的进度始终只有0,网速下行也一直 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>今天想加一个pytorch的新环境,无奈墙太厚,家里的网根本下载不下来,torch和torchvision两个包的进度始终只有0,网速下行也一直 @@ -193,13 +193,13 @@ 2020-02-15T13:42:40.000Z 2020-04-18T05:00:54.139Z -

看了一寒假的目标跟踪,一直想将自己学习到的内容整理归纳一下,迟迟没动笔(其实是打字,但说迟迟没打字比较难听哈哈)。如今快要开学了,决定还是积累一下。
本文主要是把目标跟踪中的一些相关要点做一个总结,并且按具体问题对一些主流的或者我阅读过觉得有价值的算法做一个简单概述。近年来各类方法层出不穷,且码字不易,无法涵盖所有的方法。此外,由于包含自己的转述和理解可能会存在错误。在今后的学习过程中会一直保持本文的更新,因此这将是一篇LTS的文章哈哈。

注:本文重点关注单目标跟踪。

References

参考文献:
[1]统计学习方法(第2版)
[2]Understanding and Diagnosing Visual Tracking Systems
[3]Survey of Visual Object Tracking Algorithms Based on Deep Learning
[4]Handcrafted and Deep Trackers: Recent Visual Object Tracking Approaches and Trends
[5]Review of visual object tracking technology
[6]A Review of Visual Trackers and Analysis of its Application to Mobile Robot
[7]Deep Learning for Visual Tracking: A Comprehensive Survey
[8]Video Object Segmentation and Tracking: A Survey
[9]Object Tracking Benchmark
[10]The Visual Object Tracking VOT2013 challenge results
[11]The Visual Object Tracking VOT2014 challenge results
[12]The Visual Object Tracking VOT2015 challenge results
[13]The Visual Object Tracking VOT2016 challenge results
[14]The Visual Object Tracking VOT2017 challenge results
[15]The sixth Visual Object Tracking VOT2018 challenge results
[16]The Seventh Visual Object Tracking VOT2019 Challenge Results

注:参考文献重新整理中,待补全…


简介与要求

目标跟踪是利用一个视频或图像序列的上下文信息,对目标的外观和运动信息进行建模,从而对目标运动状态进行预测并标定目标位置的一种技术。一般是在第一帧给出一个框,框中的物体就是我们需要在后续帧中用算法进行跟踪的对象。就目前的单目标跟踪而言,一般有如下要求:
monocular:我们的视频或者图片序列是仅从一个摄像头中获得的,也就是不考虑比如在城市道路场景中跨摄像头对目标跟踪的复杂应用。
model-free:没有任何先验,也就是在获取第一帧的框之前我们并不知道会框出什么物体,也不需要在之前对初始框中的物体进行建模。
single-target:只追踪第一帧框出的那一个物体,也就是除了那个物体之外所有的物体都是back ground。
casual/real-time:目标跟踪是一个在线过程,也就是不能提前获取未来的框对目标进行跟踪。
short-term:没有重检测,也就是目标跟丢了就丢了。
long-term:可以在跟丢之后重检测,这类算法一般除了跟踪之外还需要有检测的功能。

下面是目标跟踪流程的伪代码表示(不一定普适,比如有些算法不在线更新,但符合基本的过程)。


问题及挑战

通俗来讲,目标跟踪的最终目标就是要又快又准。“快”主要表现在计算量小和所需的存储空间小,“准”就是预测出的bounding box要尽可能地接近ground truth。除了上面两个基本需求(也可以说是为了更好地达到这两个基本需求),近年来的算法主要针对目标跟踪中的一些挑战进行突破,从而更好地解决某些问题之后达到更好的整体效果。
总的来说,目标跟踪的主要问题有如下这些:遮挡(occlusion)、背景干扰(background clutter)、光照变化(illumination changes)、尺度变化(scale variation)、低分辨率(low resolution)、快速移动(fast motion)、超出画面(out of view)、运动模糊(motion blur)、形变(deformation)、旋转(rotation)等。

OTB数据集依据各种问题对其中的序列进行了一个划分,这对之后针对性的研究提供了重要的参考。


生成式与判别式

利用特征判断候选样本是否为跟踪目标,可将目标跟踪的模型分为生成式模型和判别式模型,本小节就介绍一下什么是生成式模型和判别式模型。

机器学习

我们首先看看在机器学习中生成式模型和判别式模型定义的一般区分。
一般而言,机器学习的任务就是学习一个模型,应用这一个模型,对给定的输入预测相应的输出。输出的一般形式可以是决策函数,也可以是条件概率分布。
对于生成式模型,我们需要通过数据学习输入X与输出Y之间的生成关系(比如联合概率分布),也就是认为X和Y都是随机变量。典型的生成式模型有朴素贝叶斯模型、隐马尔可夫模型(HMM)、高斯混合模型(GMM)等。
对于判别式模型,我们只需要直接学习决策函数或者条件概率分布,只关心对给定的输入X我们需要输出怎么样的Y,也就是不考虑X是否是随机变量。典型的判别式模型包括k近邻、感知机、决策树、逻辑斯蒂回归模型、最大熵模型、支持向量机(SVM)、提升方法和条件随机场等。此外神经网络也属于判别式模型。
相较而言,生成式模型体现了更多的信息,不过这还是因条件而异的,不同情况不同任务两种方法各有优缺点。

目标跟踪

在目标跟踪领域,生成式模型通过提取目标特征来构建表观模型,然后在图像中搜索与模型最匹配的区域作为跟踪结果。不论采用全局特征还是局部特征,生成式模型的本质是在目标表示的高维空间中,找到与目标模型最相邻的候选目标作为当前估计。此类方法的缺陷在于只关注目标信息,而忽略了背景信息。

与生成式模型不同的是,判别式模型同时考虑了目标和背景信息。它将跟踪问题看做二分类或者回归问题,其目的是寻找一个判别函数,将目标从背景中分离出来,从而实现对目标的跟踪。

一般来说,在目标跟踪领域,判别式充分利用了目标前景和背景信息,能更加有效地区分出目标,比单单运用目标区域特征进行模板匹配的生成式模型在复杂环境中的鲁棒性更强。


算法导图

首先是参考文献[6]中的一个树状导图。

下图是中科院博士王强(github名为foolwood…呃不得不说这名字取得真谦虚)在github上上总结的Benchmark Results中的一个思维导图,同一个链接下还包括了各项成果的paper及code,值得收藏一下。

补充:这里再推荐一个在github上维护的Tracking Benchmark for Correlation Filters,按每篇论文针对或者解决的问题来分类,比较清楚,可以收藏一下。但这个仓库似乎在2017年后就没有更新了,可能是深度学习的进入或者说相关滤波系列和深度学习融合使得独立的相关滤波算法不那么突出了。

下图是浙大硕士王蒙蒙极市平台做分享的时候所用的一张思维导图,归纳得也比较清晰。

注:后两张导图中都把历年benchmark的冠军工作作了标注。

对比几张思维导图可以发现,他们都把主流算法分成了相关滤波、深度学习两个分支(或者说是基于handcrafted特征的算法和基于CNN提取特征的算法,其实近年已有所融合),此外还有一些基于强化学习、结构化SVM的模型。其实,目标跟踪算得上是计算机视觉领域中深度学习涉足较晚的一个方向,其主要原因是目标跟踪相关数据集的标注花费较大。此外,相关滤波的速度优势,也就是实时性是十分引人注目的,但在应付当前目标跟踪中的各种挑战、问题时,相关滤波的鲁棒性还是落后于深度学习方法的。
在下一节,我将结合上面几张导图,对历年尤其是近几年的算法做一个简单的整理,以方便日后的学习与研究。


各类算法的梳理与简述

本节按年份顺序对各个算法进行一个简单地梳理,其中各个算法的年份以论文发表的年份或者参加benchmark的年份为依据,可能会存在1年的区别,但影响不大。其中各年的各个算法根据算法的效果和影响大致上呈递减排序。对2013以后的算法,我拷贝了VOT challenge的结果排名,以供参照。

注意:如果你对计算机视觉或者说目标跟踪方面的一些基础方法、概念和经典算法已经有些了解,可以跳过本条建议。
考虑到在后文频繁地插入链接不太好,我就在此先推荐一下我博客的几个标签目标跟踪计算机视觉深度学习机器学习以及线性代数,其中的文章包含了一部分接下来要提到的概念和算法,可以事先浏览一下。当你在阅读时对相关概念、方法感到迷惑或者想进一步了解,博客内置的搜索功能或许能够为你提供帮助。

1981

LK Tracker

LK Tracker应该是最早的目标跟踪工作,它使用了光流的概念,如下图所示,不同颜色表示光流不同的方向,颜色的深浅表示运动的速度。

LK Tracker假定目标灰度在短时间内保持不变,同时目标邻域内的速度向量场变化缓慢。由于光流方程包含坐标x,y和时间t共三个未知数,其中时间变化dt已知而坐标变化dx和dy未知,一个方程两个未知数无法求解,因此作者假定相邻的点它们的光流具有空间一致性,即实际场景中邻近的点投影到图像上也是邻近点,且邻近点速度一致,这样就可以求解方程组了。下图是求解之后的光流向量,其中绿色箭头的方向表示运动方向,线段长度表示运动速度的大小。

光流的计算非常简单也非常快,而且由于提出得很早,各种库都有实现好的轮子可以轻松调用,但是它的鲁棒性不好,基本上只能对平移且外观不变的物体进行跟踪。

1994

KLT

KLT是一种生成式方法,也是使用了光流特征。在此基础上,作者使用了匹配角点的方法,也就是寻找边角处、纹理处等易辨识的地方计算光流来进行追踪。

1998

Condensation

Condensation(Conditional density propagation)条件密度传播使用了原始的外观作为主要特征来描述目标,采用了粒子滤波,这是一种非参数化滤波方法,属于生成式模型。它定义了一个粒子样本集,该样本集描述了每个粒子的坐标、运动速度、高和宽、尺度变化等状态;此外,通过一个状态转移矩阵和噪声定义系统状态方程。基于蒙特卡洛方法,粒子滤波将贝叶斯滤波方法中的积分运算转化为粒子采样求样本均值问题,通过对状态空间的粒子的随机采样来近似求解后验概率。

2002

Mean Shift

Mean Shift采用均值漂移作为搜索策略,这是一种无参概率估计方法,该方法利用图像特征直方图构造空间平滑的概率密度函数,通过沿着概率密度函数的梯度方向迭代,搜索函数局部最大值。在当时成为了常用的视觉跟踪系统的目标搜索方法,简单易实现,但鲁棒性较低。

2003

Feature Selection

Feature Selection利用线性判别分析自适应地选择对当前背景和目标最具鉴别性的颜色特征,从而分离出目标。

2006

Boosting

Boosting结合Haar特征和在线Boosting算法对目标进行跟踪。Boosting算法的基本思路就是首先均匀地初始化训练集中各个样本的权重,然后初始化N个弱分类器,通过训练集进行训练。第一次训练时,对第一个弱分类器,通过它在训练集上的错误率确定它的权重,同时更新训练集的样本权重(增加分类错误的样本的权重),然后,用新的训练集训练第二个弱分类器,计算它的权重并更新训练集的权重。如此迭代,将得到的分类器与它们的权重相乘,累加起来便得到一个强分类器。
上面所述是针对离线训练的,当在线训练(比如跟踪)时,为了满足实时性,就必须减少样本数量。Boosting的做法是对每一帧采集的样本仅使用一次便丢弃,然后进入下一帧采用新的样本。
以上就是在线Boosting算法的简单理解,具体而言,Boosting这里选择的弱分类器其实是Haar特征。由于Haar特征其实是一组特征,于是就需要Boosting算法根据每种Haar特征的响应来从Haar特征池中选出一个子集用于构造强分类器。

2008

IVT

IVT渐进地学习一个低维的子空间表示来自适应目标物体的变化,它将以前检测到的目标乘以遗忘因子作为样本在线更新特征空间的基而无需大量的标注样本。

2010

MOSSE

MOSSE(Minimum Output Sum of Squared Error)使用相关滤波来做目标跟踪(不是第一个,但可以看作前期的一个代表),其速度能够达到600多帧每秒,但是效果一般,这主要是因为它只使用了简单的raw pixel特征。
相比之前的算法,MOSSE能够形成更加明确的峰值,减少了漂移;此外,MOSSE可以在线更新,同时还采用了PSR来检测遮挡或者跟丢的情况,从而决定是否需要停止更新。
值得一提的是,MOSSE在做相关操作之前,对每张图都进行了减去平均值的处理,这有利于淡化背景对相关操作的影响。另外假如发生光照变化的话,减去均值也有利于减小这种变化的影响。此外要注意,输出的特征应乘以汉宁窗(一种余弦窗),用于确定搜索区域(也就是不为0的区域),且有利于突出中心的特征。

TLD

TLD(Tracking Learning Detection)主要针对long-term tracking,在跟踪的同时全局检测。它由三部分组成:跟踪模块、检测模块、学习模块。
跟踪模块观察帧与帧之间的目标的动向。作者采用了光流来跟踪,此外还提出了一种判断跟踪失效的算法,由于光流跟踪时选取的若干特征点,当其中某一个特征点的位移与所有特征点位移的中值之差过大时,也就是某个特征点离跟踪模块认为的目标中心位置很远时,就认为跟踪失效。作者还通过相似度和错误匹配度来对特征点进行筛选。
检测模块把每张图看成独立的,然后对单张图片进行目标检测定位。作者使用了方差检测器、随机森林和最近邻分类器来对目标做检测。
学习模块对根据跟踪模块的结果对检测模块的错误进行评估,当置信度较低时,重新组织正负样本对随机深林的后验概率和最近邻分类器的在线模板进行更新,从而避免以后出现类似错误。
TLD与传统跟踪算法的显著区别在于将传统的跟踪算法和传统的检测算法相结合来解决被跟踪目标在被跟踪过程中发生的形变、部分遮挡等问题。同时,通过一种改进的在线学习机制不断更新跟踪模块的“显著特征点”和检测模块的目标模型及相关参数,从而使得跟踪能够自适应,效果较之前更加稳定、可靠。

2011

FoT

FoT(Flock of Trackers)首先在目标上抓取多个interesting point并分别放入多个cell中,之后的跟踪就是检测并补偿每个cell中interesting point的偏动量,使其回到中间。如果interesting point超出cell,则让它重新恢复到cell的中点。

此外,FoT还提出了两种简单有效的failure预测方法:neighbourhood consistency predictor(Nh)和Markov predictor(Mp)。Nh的基本思想是认为正确的跟踪情况下每个local tracker给出的位移应当与它相邻的tracker相一致,而Mp主要是针对时域一致性,认为前几帧表现较好的local tracker在当前帧也会有较好的表现。FoT基于这些failure预测方法来控制模型的更新。

Struck

Struck的主要贡献是引入了结构化SVM。考虑到传统的跟踪算法将跟踪问题转化为一个分类问题,并通过在线学习技术更新目标模型。然而,为了达到更新的目的,通常需要将一些预估计的目标位置作为已知类别的训练样本,这些分类样本并不一定与实际目标一致,因此难以实现最佳的分类效果。
结合上述考虑,Struck利用了结构化SVM直接输出跟踪结果,避免了中间分类环节,这使得在当时效果有明显的提升。同时,为了保证实时性,Struck还引入了阈值机制,防止跟踪过程中支持向量的过增长。

L1 Tracker

L1 Tracker是第一个将稀疏编码引入目标跟踪问题中的算法。它把跟踪看做一个稀疏近似问题,主要是用第一帧和最近几帧得到的图像(特征)作为字典,通过求解L1范数最小化问题,实现对目标的跟踪。

MIL

MIL采用了多示例学习的方法而不是传统的监督学习(即由原本单独标记的示例变成一组示例,当且仅当所有的示例都判定为负才认为是负,只要有一个示例判定为正则整组都判定为正),对于不精准的tracker和错误标注的训练样本有鲁棒性的提升。

2012

CSK

CSK也称为核相关滤波算法,作者针对MOSSE做出了一些改进,作者认为循环移位能模拟当前正样本的所有的转换版本(除边界以外),因此采用循环移位进行密集采样,并通过核函数将低维线性空间映射到高维空间,提高了相关滤波器的鲁棒性。这里循环移位后的样本匹配可以理解为如果某个候选区域与某一个移位样本的相关操作响应较高,那么就可以理解为物体的移动和该样本移位的方式一致,从而对下一帧目标位置进行定位。
随后的工作主要从特征选择、尺度估计、正则化等方面对该算法进行改进和提高。关于循环移位和线性、非线性的核函数计算,我在之前的文章中做了一些分析,感兴趣的话可以看看。

DF

DF发现之前在图像中寻找目标的梯度下降方法首先会模糊图像来平滑目标方程,这就会严重损害目标的位置信息。因此作者提出了对每一个像素点设置多个通道,在每个通道进行卷积,这种方法同样也能平滑目标方程但不会严重损害目标的位置信息。其实这就是之后的CNN能做到的,在当时应该也算是一种创新。

CT

CT(Compressive Tracking)是一种基于压缩感知的高效跟踪算法。和一般的判别式模型架构一样,CT首先利用符合压缩感知RIP条件的随机感知矩阵对图像特征进行降维,使得到的低维信号可以完全保持高维信号的特性并可以完全重建,然后在降维后的特征上,在感知空间下采用朴素贝叶斯分类器进行分类。另外,CT在每一帧通过在线学习更新分类器,在线学习的样本来自通过相同的稀疏感知矩阵提取的前景目标和背景的特征。

ORIA

ORIA假设前一帧是完美的,于是把跟踪问题视作将下一帧图像与上一帧进行对齐,也就是一串连续的凸优化问题。

2013

下面是VOT2013的排名结果,其中Experiment 1是在所有序列上使用ground truth初始化的实验结果,Experiment 2使用含噪声(10%的尺寸扰动)的ground truth,Experiment 3使用灰度图像。

PLT

PLT通过一个固定大小的、基于二值特征向量的线性分类器对每一张图像做分类,得分最高即为目标。作者利用一个稀疏的在线结构化SVM来选出一个小的判别特征集合。在训练SVM时,考虑到在bounding box内的像素不一定都属于物体,作者使用了一种基于概率的掩模来分配权重,然后计算初始的结构化SVM,去除分值最小的特征。由于特征向量的二值性,该线性分类器可以作为查找表用于快速检测。

EDF

EDF是DF的加强版,在DF的基础上探索了每一个通道之间的联系。

LGT

LGT针对模型何时更新与更新哪些部分的问题,考虑到目标模型的整体更新会损失部分有用信息和固定的分块不利于应对目标的变化,借鉴了之前将目标有结构地分块且动态删减的思想,提出了一种由patch组成的集合构成的、用于精确定位的local layer和颜色、移位、形状三个特性组成的、用于指导增加patch的global layer。

对于新输入的一帧图像,LGT的处理流程如下:

  1. 首先使用卡尔曼滤波器结合近似匀速模型来确定目标的位置。接下来几步是对位置进行微调。
  2. 对于每一个patch,作者用一个统一的仿射变换和一个独立的微小扰动在5维空间(位置2维+尺度2维+旋转1维)定义其对于初始patch的变换。这里的$\widehat{x}$指的是初始patch。作者把$A_{t}^{G}$和$\Delta _{t}$中的参数看作正态分布的。先使用交叉熵方法反复迭代寻找最优解,当协方差矩阵的行列式小于0.1(各分量相关性很强)时停止迭代,随后把仿射变换矩阵的参数(也就是学到的正态分布的均值和方差)固定并用于所有的patch。接下来对每一个patch,用同样的交叉熵方法迭代,得到每一个patch的微小扰动的均值和方差。
  3. 结合visual consistency和drift from majority两种估计,更新每一个patch在当前帧的权重,并与前一帧的权重加权求和来确定最终的权重。这里的权重决定了每一个patch在最终所有patch混合决策时的重要性。
  4. 利用上面的patch重新计算之前卡尔曼滤波器的结果,确定目标的位置。
  5. 接着进行local layer中patch的删减与增补。对于权重小于阈值的patch作删去处理;对距离很近的两个patch进行合并,合并后产生的patch的所有参数设为合并前两个patch参数的平均。使用剩下的patch更新global layer,然后用更新后的global layer决定是否以及如何增加新的patch。为了防止突然过度增加patch,作者对增加的样本数施加上限限制,并利用加权的方式平滑调整样本容量。
  6. 进入下一帧。

LGT++

LGT++是LGT的改进版。在LGT的基础上,LGT++增加了memory、failure detection和用粒子滤波代替卡尔曼滤波的recovery机制。此外对尺度变化和背景干扰也做出了改进。

DLT

DLT是最早的基于深度学习的算法(当时AlexNet刚刚被提出),它采用了堆叠去噪自编码器网络,把跟踪视为一个分类问题,直接利用80 Million Tiny Images数据集上的预训练模型提取深度特征,这种强行task转换的训练方法存在缺陷,但在当时是个进步。

STC

STC(Spatio-Temporal Context)通过贝叶斯框架目标时间、空间的上下文信息来建模,利用得到的关系结合生物视觉系统中的注意力特性来生成confidence map来预测目标位置。由于上下文信息建模和之后的预测都采用了快速傅里叶变换,因此算法的速度很快。

文章主要举了两个例子来说明空间信息的重要性。当目标物体被部分或者完全遮挡时,周围的信息能帮助定位被遮挡的目标(假设摄像头不移动),也就是说可以利用空间的距离信息;此外,如果目标内部的两个部分比较相似(比如人的一对眼睛),就比较容易发生偏移,而如果这时恰好这两个部分的距离信息相似(距离目标中心长度相同),那就需要引入相对位置也就是方向信息来判断。
此外,考虑到生物视觉系统中的注意力特性,作者增加了一项权重函数来构成先验,该函数根据距离目标位置的远近来定义。
作者还对confidence map的参数进行了讨论,认为置信度在空间上的分布不能太平滑(增加位置模糊不确定性),也不能太尖锐(导致过拟合)。
作者认为目标的形态与近几帧有较强的关联,由此设计了时域信息模型。文中提到的时域滤波器可被证明是低通的,也就是可以滤去一定的噪声。此外,STC还设计了尺度更新方法,最终下一帧的尺度是前n帧估计尺度的均值。

LT-FLO

LT-FLO主要针对的是缺少纹理特征的目标,使用边界点来代替在目标上采集点来做跟踪。此外作者还提出了一种基于边界梯度稳定性的failure检测机制。

GSDT

GSDT提出了一种采集正负样本进行图嵌入的判别式模型。作者使用了基于图结构的分类器而不是生成一个子空间。此外GSDT还设计了一种新的图结构来区分类内的不同和样本的内在结构。

SCTT

SCTT使用了treelets降维方法,由于仅选取较高置信度的样本,相对于PCA只需要更少的样本且对噪声有更好的鲁棒性。

CCMS

CCMS(Color Correspondences Mean-Shift)用之前提到的Mean Shift方法对目标候选与目标模型、目标候选与背景模型在每种颜色(也就是直方图中每个bin)中计算相似性,反复迭代,直到收敛或者达到最大迭代次数为止,如此来进行运动估计。

Matrioska

Matrioska基于特征点提取的方法(ORB、FREAK、BRISK、SURF等),考虑到目标物体的外观变化和有利于增强模型表现力的负样本提取,使用了增枝和剪枝的方式来对目标模型进行更新。

AIF

AIF(adaptive integrated feature)提出了一种评估特征稳定性的方法,并根据不同特征的稳定性动态地分配权重。

HT

HT借鉴霍夫森林,也就是霍夫变换结合随机森林,相比一般的随机森林增加了位移信息。HT将目标分割成多个图像块,这些图像块含有它们各自偏离目标中心的向量$d$,对每个图像块提取特征描述子,这样就构成了正样本,即$y=1$;在图像的其他区域也提取同样尺寸的图像块,也提取特征描述子构成负样本,即$y=0$,注意负样本的偏移向量$d=0$。由此训练生成树,再由树构成森林。
在跟踪时,将每个图像块输入训练好的森林里,最终会落到森林的每棵树的一个叶节点上,这就得到了该图像块相对目标中心的偏移向量$d$以及概率$p$,随后将每棵树上的结果加权平均,得到了该图像块的结果。最后将所有的目标分割出的图像块的结果组合起来,得到目标预测结果。
HT的一个好处就是可以调整bounding box的长宽比,此外作者认为这对非刚性或者铰接的目标也有很大好处。

STMT

STMT把目标跟踪分成镜头运动估计和目标运动估计两个阶段,先估计摄像头的运动并进行对齐,然后再定位下一帧的目标位置。

ASAM

ASAM(Adaptive Sparse Appearance Model)用一个样本集来表示目标的各种变化,并且基于判别式和生成式的稀疏表示,使用了第一帧、后续帧两阶段的在线跟踪算法。

2014

下面是VOT2014的排名结果,这里的A表示accuracy,R表示robustness。

DSST

DSST主要考虑了尺度缩放的问题。它将目标跟踪看成位置变化和尺度变化两个独立问题,提出了一个高、宽、尺度数三维的滤波器,使用先计算平移位置再聚集尺度的“两步”法,即训练了两个滤波器,首先训练位置平移相关滤波器以检测目标中心平移,然后训练尺度相关滤波器来检测目标的尺度变化。

CN

CN(Color Naming)考虑到在遇到光照变化、形变、部分遮挡、背景干扰等问题时,颜色特征相比灰度特征能提供更丰富的信息以取得更好的效果,引入了颜色特征来扩展CSK,它将目标RGB(红绿蓝)三维空间的颜色特征映射为黑、蓝、棕、灰、绿、橙、粉、紫、红、白和黄11维空间的颜色特征的多通道颜色特征,后又降维至2维以保证实时性。Color Naming较RGB三原色特征更符合人类的感觉,对目标的表征能力更强,而且具有一定的光学不变性。

SAMF

SAMF也考虑了尺度问题,思路比较简单,采用k个尺度去采样,由于核相关操作的点乘需要固定尺度的输入,因此对采集到的样本作双线性插值成为固定尺度,然后再做相关操作。在特征方面,SAMF发现HOG和Color Naming有互补作用,考虑到和相关操作仅包含点乘和向量范数的计算使得多通道很容易被引入,因此使用了HOG和Color Naming多通道特征。

KCF

KCF跟CSK是同一个团队提出的,它跟CSK的区别是就是作者对循环性质进行了完整的理论推导,引入HOG特征并提供了一种把多通道特征融合进相关滤波框架的方法,对CSK作了进一步的完善,是一个具有里程碑意义的工作。算法的详解和一些数学理论可以看看我之前的文章。

DCF

DCF与KCF出自同一篇paper,不同的是KCF使用的是高斯核,DCF使用的是线性核。

注意:这里的DCF(Dual Correlation Filter)和之后一些文章中提到的DCF(Discriminative Correlation Filter)是两个不同的概念,请注意,别搞错了。

FCT

FCT(Fast Compressive Tracking)和CT一样,也是使用了压缩感知,主打速度。相比之前,FCT提出了一种由粗到精的搜索策略,而不是穷尽搜索。首先在一个较大的搜索半径内选择一个较大的搜索步长,得到一个粗糙的位置,然后以该位置为中心,在一个较小的搜索范围内,以一个较小的搜索步长进行搜索,最后得到跟踪目标的位置,这样就能在不降低最终精度的前提下加速寻找过程。由于可证明CT特征具有尺度不变特性,FCT在采集候选区域时增加了尺度因子,即在同一位置采集三个尺度的候选区域,从而得到当前帧的尺度。此外,作者采用了每5帧更新一次尺度的策略。

CMT

CMT用成对的特征点之间角度的变化来判断目标的旋转情况,此外还用特征点投票的方式来确定目标的位置。为了避免尺度变化引起的投票不准,也就是从特征点出发指向目标位置的投票向量越过了目标中心或者没达到目标中心,作者用欧氏空间和原图像空间之间各对特征点之间距离的比值来进行修正。基于投票出的目标位置的聚类,作者还给出了一种一致性的判别方法,将聚集最多数特征点的投票位置视为一致性聚类,并将投票至其他位置的特征点视为错误从而移除。

2015

下面是VOT2015的排名结果。

SO-DLT

SO-DLT针对DLT的缺陷进行改进,使得CNN更加适用于目标跟踪。由于目标跟踪的目的是将物体从背景中分离出来而不是全图识别,DLT的训练方法和标签化输出就不是很合适了。
但是,由于当时跟踪方向标注数据的匮乏,作者还是不得不使用ImageNet图像检测数据集来进行预训练,事实证明这是有效的,因为目标检测和目标跟踪两个不同的task中存在一样的共性信息。不同于标签化的单个数值输出,SO-DLT输出的是一个50x50的像素级的概率图。
由于上述训练方法训练的是CNN从非物体中提取出物体的能力,因此在实际跟踪接收到第一帧时,还需根据目标对网络进行微调,否则会跟踪出视频或者图像序列中所有的无论是目标与否的物体。
类似DSST,SO-DLT采用的是先预测目标中心位置,然后再从小到大确定尺度的策略,如若扩展到预设的最大尺度来检测的概率图依旧达不到要求,则认为已经跟丢目标。
此外,为了提升鲁棒性,作者采用了两个CNN网络共同决策而以不同方式更新的策略。两个网络分别针对的是short-term和long-term。针对short-term的网络在负样本的概率图响应和超过一定阈值时进行更新,为的是防止负样本与目标响应近似而导致漂移;针对long-term的网络在当前帧预测结果的置信度达到一定水平以上时才进行更新,因为此时可认为框出的目标较为可信。
每次更新时需要采集正负样本,SO-DLT对正负样本的提取方法比较简单,在目标位置及周围形成一个类似九宫格的区域,在中间格用四种尺度提取正样本,对周围的8格采集负样本。

MDNet

MDNet设计了一个轻量级的小型网络学习卷积特征表示目标。作者提出了一个多域的网络框架,将一个视频序列视为一个域,其中共享的部分用来学习目标的特征表达,独立的全连接层则用于学习针对特定视频序列的softmax分类。
在离线训练时,针对每个视频序列构建一个新的检测分支进行训练,而特征提取网络是共享的。这样特征提取网络可以学习到通用性更强的与域无关的特征。
在跟踪时,保留并固定特征提取网络,针对跟踪序列构建一个新的分支检测部分,用第1帧样本在线训练检测部分之后再利用跟踪结果生成正负样本来微调检测分支。
此外,MDNet在训练时还采用了难例挖掘技术,随着训练的进行增大样本的分类难度。

SRDCF

SRDCF主要考虑到若仅使用单纯的相关滤波,可能会存在边界效应,也就是相关滤波采用循环移位采样导致当目标移位到边缘时会被分割开,此时得到的样本中就没有完整的目标图像从而失去效果。
于是,作者采用了大的采样区域,用两倍区域进行循环移位,这就保证了无论如何移位,获得的样本中都能有一个完整的目标存在。
然而,由于上述移位方式会导致背景信息被夹在横向以及纵向的两个样本之间而出现在图片的中间区域,这会导致模型判别力的退化。因此作者在移位之前先在滤波器系数上加入权重约束(类似于惩罚项):越靠近边缘权重越大,越靠近中心权重越小。这就使得滤波器系数主要集中在中心区域,从而让背景信息的影响没有那么明显。尽管作者采用了埃尔米特矩阵的共轭对称性来提升计算量,但是由于SRDCF破坏了原本闭式解的结构使得优化问题只能通过迭代求解,因此速度比较缓慢。

DeepSRDCF

DeepSRDCF在SRDCF的基础上,将handcrafted的特征换为CNN的特征,关注点也在解决边界效应。作者使用DCF作为网络的最后一层,也就是对之前卷积网络输出的每一个通道的CNN特征都训练一个滤波器用于分类。作者还对不同的特征进行了实验,说明了CNN特征在解决跟踪的问题采取底层的特征效果会比较好(DeepSRDCF仅用了PCA降维处理的第一层),说明了跟踪问题并不需要太高的语义信息。

HCF

HCF的主要贡献是把相关滤波中的HOG特征换成了深度特征,它使用的是VGG的3、4、5三个层来提取特征,针对每层CNN训练一个过滤器,并且按照从深到浅的顺序使用相关滤波,然后利用深层得到的结果来引导浅层从而减少搜索空间。

FCNT

FCNT较早地利用CNN网络底层和顶层不同的表达效果来做跟踪。不同于以往的工作把CNN看成一个黑盒而不关注不同层的表现,FCNT关注了不同层的功能,即发现:顶层的CNN layer编码了更多的关于语义特征的信息并且可以作为类别检测器;而底层的CNN layer关注了更多局部特征,这有助于将目标从目标中分离出来。这个发现在之后的许多工作中也得到了应用和体现。如下图所示,这里的a图表示的是ground truth,b图表示的是使用VGG的conv4-3,也就是第10层产生的热力图,c图是通过conv5-3也就是第13层产生的热力图。

可以看到,较低维的CNN layer(conv4-3)能够更精准地表示目标的细粒度信息,而较高维的CNN layer(conv5-3)热力图显示较模糊,但对同类别的人也做出了响应。这就是说,顶层缺少类内特征区分,对类间识别比较好,更适合作语义分割;底层则反之,能够更好地表达目标的类内特征和位置信息。
基于不同层(顶层和底层)之间提取特征的不同,作者提出了一种新的tracking方法,利用两种特征相互补充辅助,来处理剧烈的外观变化(顶层特征发挥的作用)和区分目标本身(底层特征发挥的作用)。由于feature map本身是有内在结构的,有很多的feature map对目标的表达其实并没有起到作用,因此作者设计了一种方法来自动选择高维CNN(GNet)或者低维CNN(SNet)上的feature map,同时忽略另一个feature map和噪声。在线跟踪时,两个网络一起跟踪,采用不同的更新策略,并在不同的情况下选择不同的网络输出来进行预测。
顺便提一下,为了简化学习任务,降低模型复杂度,作者采用了稀疏表示的方法。
关于FCNT的一些相关概念和具体按步骤的细节实现,可以参考一下我之前写的文章。

LCT

LCT主要针对的是long-term tracking的问题。作者配置了一个detector,用于跟丢之后快速重检测。LCT用了两个滤波器,一个是用于平移估算的$R_{c}$,使用padding并施加汉宁窗(一种余弦窗),结合了FHOG和一些其他的特征;另一个是用于尺度估计的$R_{t}$,不使用padding和汉宁窗,使用HOG特征,此外$R_{t}$还用于检测置信度,用来决定是否更新模型和是否重检测。

CCT

CCT借鉴KCF中kernel trick的特性和DSST中将定位和尺度估计两步分离的思想,对DSST只利用本来的特征空间表征目标的不足进行改进,在核特征空间对目标进行表征并用尺度因子扩展KCF的核相关滤波器,也就是为了保证计算效率和连贯性,利用尺度因子将每一帧目标尺度都统一为初始帧的目标尺度,然后使用核相关滤波器进行位置估计。然后,通过和DSST一样的方式再进行尺度估计。
为了应对漂移的问题,CCT使用了一个在线CUR滤波器。CUR矩阵分解可以近似的表示原矩阵A,其中C是A的列而R是A的行,两者通过一种固定的方式从A中随机采样形成,这既保证了A的内在结构,可以反映A的低秩属性,也可以看作一种映射,即将过去的目标表征矩阵投影到一个可被证明具有误差上界的子空间。此外,作者还引入了一个基于失败检测的自适应学习率调整方法。

CFLB

CFLB讨论了循环移位带来的边界效应的问题,提出了在目标外围扩大尺寸来进行循环移位的方法,使得有效样本的比例大大提高。具体来说,就是给原来的循环移位样本左乘一个列数远大于行数的掩模矩阵。此外,由于扩大尺寸后部分参数要在空间域而不是频域计算会导致效率降低,作者利用了增广拉格朗日方法(即在拉格朗日方法的基础上添加了二次惩罚项,从而使得转换后的问题能够更容易求解,不至于因为条件数变大不好求)来解决这个问题。

KCFDP

KCFDP借鉴了目标检测中detection proposal的思想(主要用于减少计算和提高质量),来解决之前DCF系列算法中的尺度和长宽比变化的问题。对每一帧,KCFDP首先用KCF对上一帧输入(准确的说是之前每一帧的加权累积)作操作,得到当前帧的位置和响应分数$v$;随后利用EdgeBoxes(一种detection proposal方法)在KCF预测的位置周围搜寻proposal,选取其中的前200个,并排除其中与之前KCF得出的预测目标IoU大于0.9(认为结果一样,无需考虑)或者小于0.6(认为误判,不是目标);最后,在剩余的proposal中,选择得分最高的proposal,与之前的响应分数$v$作比较:若小于$v$,则把KCF的结果作为预测结果且不更新尺度和长宽比(KCF算法本身具有该功能);若大于$v$,则把该proposal作为预测结果,并利用该proposal的尺度和长宽比来更新目标的参数。

HCFT

HCFT构造了一种阶梯式的深层至浅层由粗到细的定位方法,结合深层网络的语义信息和浅层网络的高分辨率位置信息,其网络结构如下(和FPN很像)。

为了保持分辨率相同,作者对池化后的深层输出再进行双线性插值以复原原来的分辨率。假设$l$层最大响应处的坐标为$(\widehat{m},\widehat{n})$,HCFT通过以下式子来确定$l-1$层目标的位置。

第二行的约束是为了浅层细粒度的位置需保持在深层粗粒度的位置附近,这样便完成了由粗到细的定位方法。
此外,在训练过程中需要采集正负样本,由于正负样本边界难以区分的模糊性和二值(也就是0,1标注)的正负样本的绝对性导致一点微小的正负样本区别就会导致drift。为此,作者将训练样本的标注回归到高斯方程的平滑标签。

MUSTer

MUSTer模拟了人脑的记忆过程,类似于LSTM那样分成short-term和long-term两种memory,使用了相关滤波(short-term)和特征点检测(short-term+long-term),最后根据两种记忆形式的提供的输出来决策和进行滤波器的更新。
人脑的记忆分为感官、短时记忆、长时记忆三个阶段,MUSTer的设计基本采用了这样的三个step,如下图所示。

MUSTer的结构比较“纵横交错”,下面我对一些比较重要的部分做一个概述。
短时记忆和长时记忆都由特征点的集合构成。特征点数据集包括目标和背景两种样本,其中背景主要是用于遮挡的判断,即当位于bounding box中的背景特征点与目标特征点的比例超过一定值时,认为此时发生了遮挡。
对于特征点的匹配,作者采用了最近邻方法,在欧几里得空间根据余弦相似度(也就是两个向量余弦夹角的大小,角度越小,余弦值越大,相似度越高)来计算匹配置信度。为了判别离群值(outlier),还需计算第二近邻的相似度,如果第一近邻的相似度比上第二近邻的比值小于某个阈值,就说明该处特征点比较集中应该不是离群值。
长时记忆模块通过RANSAC估计的一个版本——MLESAC(引入似然度)来决定目标的状态,从而与相关滤波的输出结合。
短时记忆在每一帧都进行更新,若根据前面所说的方法判定为遮挡,则清空短时记忆;若此时并没有判定为遮挡,则用RANSAC估计输出的内围值(inlier)来替换之前的短时记忆。此外,为了避免多余的特征点出现,作者用网格划分目标template并根据相对位置分配ID,若出现重复的ID,则认为两者中之前的特征点是多余的。
长时记忆只在判断跟踪成功和无遮挡时进行更新。作者认为匹配失败的特征点能够表示目标发生变化的重要信息,因此长时记忆更新是针对匹配失败的点进行的,将匹配失败且位于bounding box外面的点移入背景数据集,而将匹配失败且位于bounding box内部的点移入目标数据集。模拟人脑,长时记忆采用的是一种对数形式下降的遗忘曲线。

RPAC

RPAC将相关滤波器应用于分块跟踪,并且借鉴了粒子滤波中的贝叶斯估计的思想,在提升鲁棒性的同时保证了速度。
作者对每一区块(part)都使用一个独立的相关滤波器,使用了PSR(体现置信度)和时域顺滑程度(用于判断遮挡等情况)两者结合来分配每一区块的权重,同时仅当这个权重大于阈值时才更新对应的滤波器以达到自适应更新的效果。这里每一个区块的输出都是一个confidence map,最后需要根据权重和相对位置组成一个大的confidence map用于接下来的预测。
为了防止部分区块漂移的问题出现,作者采用了贝叶斯估计框架,即选择使得状态(一组仿射运动参数)先验值最大的候选区域作为结果,考虑到多个confidence map之间重叠的部分直接求和会出现叠加的较大值而影响概率估计,作者根据每一个区块的最大响应值和尺寸来施加余弦窗,从而抑制多张图中较小值的叠加改变某些区块位置上的响应分布。
此外,对于偏离较远的区块,RPAC采用自动丢弃并利用其他区块重新生成的方法。得益于分块的方式,使得tracker对尺度变化也有适应性。

RPT

RPT把目标物体看作一系列具有相似运动轨迹的patch的集合。作者基于粒子滤波的框架,用patch的两个属性来定义每个patch的可靠性:可追踪性和目标附着性。可追踪性直接用KCF输出的响应图取PSR来定义;目标附着性根据每个patch在近k帧的运动轨迹来定义,具体来说,就是认为正样本与其他正样本的运动轨迹是一致的且远离负样本,同时负样本与正样本的运动轨迹有很大的差别。其公式定义如下,其中$y_{t}$是正样本或者负样本的$\pm 1$标签(正负样本用bounding box来分割)。

由于粒子滤波是用函数(这里是可靠性函数)对后验概率分布做近似,由于无法达到理想状态(即完全一致),因此为了避免噪声的累积,粒子滤波类算法需要重采样来不断补入正确的信息。RPT采用的是不断补入新样本的方法而不是全部重采样替换的方法,作者在两种情形下采样新的样本:当正样本或者负样本其中一者的比例过高时,采集新样本来平衡;当跟踪置信度(这里定义为PSR)较低时,采集新样本,这种情况往往出现在遇到缺乏纹理的目标物体时。此外,作者对离目标过远的patch进行舍弃,具体就是划定一个比bounding box更大的矩形框,对超出这个矩形框的patch做舍弃。
可以说,作者把patch分成三类。第一类是positive patch,也就是在目标上的;第二类是贴着目标周围一圈的negative patch,这对下一帧区分物体和背景很有帮助;第三类就是离目标很远的negative patch,这些patch的作用就比较弱,因此舍弃。

2016

下面是VOT2016的排名结果。

DLSSVM

DLSSVM延续之前的Struck,利用结构化SVM,在优化的阶段做了一些改进进行提速。其实结构化SVM分类器非常强大,但是因为它求解优化的过程比较复杂以及使用稠密采样(粒子滤波或者滑窗采样)比较耗时,使得结构化SVM的速度成为一个瓶颈,因此不如一些使用相关滤波的SOTA的算法。

C-COT

C-COT(连续空间域卷积操作)发现单一分辨率的输出结果存在扰动,因此作者想到利用CNN中的浅层表观信息和深层语义信息相结合。然而之前的DCF系列算法仅能使用单一分辨率的特征图,这就无法使用预训练CNN中不同分辨率的不同层,这是限制其效果的重要因素。因此,作者提出一种连续周期的插值运算符以利用不同空间分辨率的响应,在频域进行插值得到连续空间分辨率的响应图,最后通过迭代求解最佳位置和尺度(用0.96,0.98,1.00,1.02,1.04五种缩放倍率去搜索)。
和名称一样,C-COT最重要的贡献是它把图像$N_{d}$个像素点的离散分布变成了周期为$T$的连续空间响应图。由于本文的理论功底很深,我之前并没有看懂,后来经学长的指点才有所感悟。事实上,这里的插值运算符做的并不是插值的事,而是用一种近似的方式将离散信号重构为连续信号。虽然会导致计算量剧增,但这是一个很重大的突破。
文中还提出了一种使得各个分辨率通道的特征自然融合至相同分辨率的方法,这里相同分辨率可理解为最后的各个响应图在空间上拥有相同的样本点数。作者首先对各个不同分辨率的通道进行插值,然后使用对应的滤波器在连续的空间域内卷积,最后将响应求和得到最终的置信度方程。
C-COT使用的是类似SRDCF的框架,也引入了空间正则项,当远离目标中心是施加较大的惩罚,这使得能够通过控制滤波器的大小来学习任意大小的图像区域。C-COT在每一帧也会采集一个训练样本,根据过去帧数的远近来设置每个采集样本的重要性权重(每次都做归一化),并且设置了最大的样本容量,当超出容量时删去重要性权值最小的样本。不同于SRDCF使用Gauss-Seidel迭代法,C-COT使用Conjugate Gradient方法来提高效率。
得益于浅层特征的高分辨率,C-COT能够达到sub-pixel的精度,也就是仅次于像素级别的精确度。位置细化的过程就是上面所说的用共轭梯度法迭代的过程,在C-COT的代码中有一个迭代次数设置,被设置为1,即就使用一步迭代优化后的位置。换句话说,在当前长时跟踪算法本身误差之下,更精细的位置意义不大。

SRDCFdecon

SRDCFdecon针对在线跟踪时采集的样本中有一部分质量不佳的问题,不同于之前把采样和样本的选择作为一个独立的模块,作者提出了一种将样本权重统一到模型参数中的损失函数。
不同于之前“加入训练集or舍弃”这样二选一的样本选取方式,SRDCFdecon使得样本的重要性权重连续,同时在跟踪的过程中能够完成权重的重新分配和先验的动态变化。这里的先验其实可以看作对权重的一种约束,使权重在近几帧逐渐增大。作者用一种比较巧妙的方式来控制先验的影响力,具体来说,对权重的正则项为$\frac{1}{\mu }\sum_{k=1}^{t}\frac{\alpha _{k}^{2}}{\rho _{k}}$,当$\mu$趋向于无穷时,损失函数求解得到仅有当前帧的权重$\alpha$趋向于1,相当于丢弃了之前的样本,仅接收并保存当前帧的新样本;当$\mu$趋向于零时,$\alpha$将趋向于先验$\rho$。

Staple

Staple提出了一种互补的方式。考虑到HOG特征对形变和运动模糊比较敏感,但是对颜色变化能够达到很好的跟踪效果,color特征对颜色比较敏感,但是对形变和运动模糊能够有很好的跟踪效果,因此作者认为若能将两者互补就能够解决跟踪过程当中遇到的一些主要问题。于是,Staple使用HOG-KCF与color-KCF结合算法对目标进行跟踪,速度很快,效果也很好。

SINT

SINT运用匹配学习的思想,最早地把孪生网络(Siamese Network)应用于目标跟踪。它通过孪生网络直接学习目标模板和候选目标的匹配函数,并且在online tracking的过程中只用初始帧的目标作为模板来实现跟踪。

TCNN

TCNN使用一个树形的结构来处理CNN特征。作者利用可靠性来分配预测目标的权重,采用的更新策略是每10帧删除最前的节点,同时创建一个新的CNN节点,选择能够使新节点的可靠性最高的节点作为其父节点。这样一直保持一个active set,里面是10个最新更新的CNN模型,用这个active set来做跟踪。TCNN效果较之前有一定提升,但是速度比较慢,而且比较消耗存储空间。

SKCF

SKCF把之前KCF中用于确定搜索区域的余弦窗换成了高斯窗,这么做有两点主要的好处。
首先,当搜索区域固定时,余弦窗的带宽就是固定的了,而高斯窗则可以通过调整方差来改变中间响应比较高的区域的宽度。可以这么理解,我们从二维的余弦函数和高斯函数来看,假设搜索区域的宽度是$\pi $,那么距离边缘$\frac{\pi }{6}$的位置一定是中间最大值下降一半的位置,而高斯函数的形状则还受方差控制。这一特性使得高斯窗在搜索区域确定时能够更好地适应目标尺寸,此外论文中还提到了这种方法能方便减轻计算量。
此外,高斯分布的傅里叶变换依旧是高斯分布。这种特性可以抑制频谱泄露的问题。简单来说,频谱泄露就是信号从时域转化到频域后,除了原本应该出现的谱线,其旁边还会漏出一些小的频谱,从而造成干扰。因此,抑制频谱泄露能够保持前景与背景在频域内的区分度。
SKCF还改进了之前一些基于特征点的算法的不足。由于之前的一些算法在最终决策时用矩形框来划定有效的特征点,并分配相同的权重做出决策。作者认为这样相当于间接的认为目标是矩形的,而大多数目标的几何结构往往不是矩形。于是SKCF对各个特征点采用从中心到周围逐渐减少权重分配的方式,能更好地适应目标的几何结构。
SKCF还是用了英特尔CCS(复数共轭对称)文件格式,无论是用在SKCF还是KCF上,计算速度相比原来的KCF都提升了将近一倍,或许是共轭对称减少了一半计算量。

MRF

MRF提出了一种基于马尔科夫随机场的模型,挖掘各个patch和目标之间的联系(弹性能量)并判断每个patch的遮挡情况。作者发现当响应值较低时并不能认为该patch被遮挡了,也有可能是目标外观变化等情况。此外作者还发现当发生遮挡时,会出现较大的响应值分散分布的现象。于是当patch中响应值高于$\eta s_{max}^{k}$的像素个数占比高于阈值时(这里的$\eta$是一个小于1的因子),就认为该patch被遮挡。
对于尺度变化,作者发现当尺度变小时patch之间的重叠会变多,因此通过计算初始帧的patch距离和当前帧的patch距离之比来确定尺度缩放的比例。
不同于之前的马尔科夫随机场模型,作者在文中还给出了一种高效的置信度传播方法。

GOTURN

GOTURN提出了一种类似孪生网络的框架,将裁剪过的前一帧和当前帧分别通过在ImageNet上预训练的backbone提取特征,然后用三层全连接层进行特征比较并进行回归。其中前一帧的图像在裁剪时将其置于裁剪后图像的中心,并在周围做一定的扩充以吸收更多上下文信息。当前帧裁剪区域也就是搜索区域由上一帧的位置来确定。
GOTURN最突出的贡献就是把基于深度神经网络的速度第一次达到了100FPS。作者通过在视频和静态图像上进行离线训练,在跟踪时不更新来达到这种效果。由于训练集的缺乏,作者冻结之前在ImageNet上预训练的CNN权重,在离线训练时仅更新FC层。考虑到实际跟踪时的特点,作者还设计一种平滑的运动模型。作者发现目标相邻两帧中心点的坐标关于尺度的增量呈均值为0的拉普拉斯分布(类似高斯分布,只不过中间是尖的),也就是说一般物体逐帧的运动是较小的。因此,作者对训练数据做增广处理,使得随机裁剪得到的样本能服从拉普拉斯分布。由于训练方式和网络设计,使得GOTURN仅对目标敏感而不是对类敏感(比如行人检测能检测各种行人而不能检测车)。

SiamFC

SiamFC使用孪生网络来解决数据稀少和实时性要求对深度学习在目标跟踪中的限制。作者以第一帧的BBox的中心为中心裁剪出一块图像,并将其缩放至127x127作为template,并保持不变。在后续帧中,search image也用类似的方法得到。分别将template和search image通过5层不带padding且不在线更新的AlexNet,然后用互相关层做相关操作得到输出的score map。响应最大值和中心的偏差表示位移。此外作者用一个小批量的不同尺度的图像去前向传播来实现多尺度判断。
由于对于search image来说,表示通过CNN的卷积方程是全卷积的,因此可以使用比template大的search image,也就是可以在它上面的各个子窗口进行计算。
基于卷积的平移等变性,我们可以通过score map得到目标的位置,即当目标平移了$n$时,相应的就会在score map上平移$\frac{n}{stride}$。之所以仅使用5层而不是更深的网络,是因为SiamFC没有使用padding来使得网络能够更深。之所以不使用padding,是因为一旦加入了padding,会使得图像边缘像素的响应值在平移的同时会发生改变,这就不利于最后的定位了。具体来讲,就是当目标处于画面中央时,padding进来的是context;而当目标处于画面边缘时,padding进来的就是0了,这种信息的不同会影响定位和目标的判断。

2017

下面是VOT2017在隐藏数据集上的排名结果。

ECO

ECO(高效卷积算子)主要是为了解决C-COT速度慢的问题。从参数降维、样本分组和更新策略三个角度对其改进,在不影响算法精确度的同时,将算法速度提高了一个数量级。
为了减少模型参数,考虑到在C-COT中许多滤波器的能量(可理解为贡献)小得几乎可以忽略,因此ECO使用了一个较小的滤波器子集,且原来的滤波器都可以用这个子集中的滤波器线性组合表示(即乘上一个行数为高维数、列数为低维数的矩阵)。子集的选取方法就是简单的选取能量高于某一阈值的滤波器,其效果类似于PCA。作者提出了一个因子化的卷积算子来学习这个子集,用PCA初始化,然后仅在第一帧有监督地优化这个降维矩阵,在之后的帧中直接使用,相比C-COT模型参数量大大降低,同时也减轻了计算和存储的负担。

为了减少样本数量,作者提出了一个紧凑的样本空间生成模型,采用高斯混合模型(GMM,可理解为当有多个聚类时用多个不同的高斯模型来表示更好)来合并相似样本。当GMM的聚类(component)数量超过阈值时,如果有权重低于阈值的component,则丢弃之;否则,就合并最近的两个component。如此就可以建立更具代表性和多样性的样本集,既保持样本之间的差异性,也减少了存储的样本数量。

此外,作者还提出了一种稀疏的更新策略,即每隔N帧(实验发现5帧左右最好)才更新一次参数。由于样本集是每帧更新的,这种稀疏更新策略并不会错过间隔期的样本变化信息。此外,这种方法的另一个好处就是把原本的用逐帧单独样本进行更新,变成了用连续几帧所采集的样本所构成的batch来进行批处理的更新,这样就减小了在某帧遮挡、突变时过拟合的可能性。由此,稀疏更新策略不但提高了算法速度,而且提高了算法的稳定性。

CREST

CREST提出了将DCF构建成一层卷积神经网络,并且引入了残差学习来应对目标外观变化带来的模型退化。
考虑到之前的DCF系列没有发挥端到端训练的优势和空间卷积与相关滤波中循环输入点乘的相似性,作者用一层卷积神经网络来代替DCF的作用,这不仅使得模型能够通过反向传播训练,同时因为没有使用循环移位,避免了边界效应。
由于上述一层网络难以达成在多种情况下网络输出和ground truth的一致(模型复杂度较低易受干扰),而若使用多层网络很可能会导致模型退化(我理解为过拟合导致的),作者引入空间残差和时间残差。设我们希望最佳的输出为$H(x)$,而上述单层网络的输出是$F_{B}(x)$,为了补足某些时候(尤其是复杂情况下)单层网络的输出与希望最佳的输出之间的差距,引入残差项$F_{R}(x)=H(x)-F_{B}(x)$。在训练时,$F_{B}(x)$和$F_{R}(x)$中的参数一起训练,使得遇到特殊情况(遮挡、运动模糊等)时,$F_{R}(x)$能够补足纠正$F_{B}(x)$不稳定的响应结果。
空间残差和单层网络都是利用当前帧作为输入,考虑到空间残差有时候也会失效,作者又引入了把初始帧作为输入的时间残差,最终表达式如下:

注意:论文中第一项为$F_{R}(X_{t})$,可能有误,为此我作了修改。

另外,CREST采用当前帧最大响应尺度和上一帧尺度加权求和的方法来决定当前帧的最终预测尺度,从而使尺度能够平滑地变化。

LMCF

LMCF借鉴了KCF的循环特征图、Struck的结构化SVM,使用相关滤波在频域加速计算,从而解决了之前结构化SVM系列算法(Struck、DLSSVM)的速度问题。
在前向追踪时,LMCF考虑到画面中相似物体的干扰,提出了一种多峰值的目标跟踪算法(Multimodal Target Tracking),即对高于某一阈值的响应峰值做二次检测,把response map和一个用于筛选的二值矩阵作点乘,相当于把不是峰值的位置滤为0。对于通过筛选的峰值,以每个峰值为中心提取patch并用之前的方法再计算一遍峰值,取此时最大的峰值作为结果。
在模型更新时,LMCF提出了一种高置信度的更新策略(High-confidence Update),由于LMCF主要关注的是实时性,所以希望在算法简单的情况下能够减少失误。在传统的方法中,一般是当最大响应的峰值高于某一个阈值时(认为没跟丢目标),就对模型进行更新;否则若没有响应值超过峰值,就不对模型进行更新。而该工作的实验发现,当目标被遮挡时,响应图会震荡得非常厉害(存在多个较大的峰值),但同时最大响应的峰值仍旧会很高,这就会指导模型进行错误的更新并导致最后跟丢目标。于是作者提出了一个APCE值,定义如下。

只有当最大响应的峰值比较明确,即远超response map中的其他的响应时,APCE值才会比较大。因此LMCF仅当最大峰值和APCE超过阈值时才允许对模型进行参数和尺度模型的更新。

DeepLMCF

同LMCF,不同之处是使用了CNN特征。

MCPF

MCPF结合多任务相关滤波器(MCF)和粒子滤波器,这里的多任务相关滤波器指的是利用了多种特征滤波器之间的相关性。作者对K种特征,定义了参数$z_{k}$去选择具有判别力的训练样本。作者发现,各个特征中的$z_{k}$往往会选择具有相同循环移位的样本,因此不同的$z_{k}$应该具有相似性和一致性。为此,作者在损失函数中增加了矩阵Z的混和范数。
考虑到粒子滤波通过密集采样来覆盖状态空间中的所有状态,这会大大增加计算量,而且并不能保证很好地包括目标物体在一些情况下的状态。因此作者利用MCF对每个采样的粒子进行引导,使其更接近目标的状态分布。这样就可以在提升效果的同时每次采集较少的粒子,从而提高计算效率。另一方面,粒子滤波的密集采样能够在目标尺度发生变化时覆盖状态空间,这就解决了单一相关滤波器的尺度问题。
算法的流程分为四步:
首先,使用转移模型生成粒子并且重采样。
然后,使用MCF对粒子进行微调,使其转移到比较合适的位置。
接着,利用响应更新MCF的参数。
最后,通过求样本均值问题来决定目标的状态,也就是位置等参数。
可见,这里相关滤波仅起到指导的作用,最后的决策由粒子滤波器做出。
顺便一提,MCPF使用了Accelerated Proximal Gradient来解决这里不可微分的凸优化问题(含有范数)。

CFNet

CFNet结合相关滤波的高效性和CNN的判别力,考虑到端到端训练的优势,从理论对相关滤波在CNN中的应用进行了推导,并将相关滤波改写成可微分的神经网络层,将特征提取网络整合到一起以实现端到端优化,从而训练与相关滤波器相匹配的卷积特征。
CFNet采用孪生网络的架构,训练样本(这里指用来匹配的模板)和测试样本(搜索的图像区域)通过一个相同的网络,然后只将训练样本做相关滤波操作,形成一个对变化有鲁棒性的模板。为了抑制边界效应,作者施加了余弦窗并在之后又对训练样本进行了裁剪。
在对比实验中作者发现仅使用一层卷积层时CFNet相比Baseline+CF效果提升最显著,对此作者的解释是可以把相关滤波层理解为测试时的先验知识编码,当获得足够的数据和容量时(增加CNN层数时),这个先验知识就会变得冗余甚至是过度限制。

2018

下面是VOT2018 short-term的排名结果。

STRCF

STRCF(时空正则相关滤波器)主要针对SRDCF的速度做出改进,同时在精度上也有很好的提高。作者发现SRDCF速度很慢的两个原因是:每次对多张图片进行训练打破了循环矩阵的结构,从而无法发挥循环矩阵的计算优势;巨大的线性方程组和Gauss-Seidel迭代法没有闭式解,效率较低。对此,STRCF提出了引入时间正则和ADMM算法。
受online Passive-Aggressive learning的启发,STRCF在SRDCF空间正则的基础上引入了时间正则。我们可以对比两者的回归求解公式具体来看一下。
SRDCF:

STRCF:

这里的$w$表示空间正则化矩阵,越靠近边缘值越大;$f$表示相关滤波器,$f_{t-1}$表示的是$t-1$帧时的滤波器。
忽略每项之前的常数系数,我们可以看到两式的第二项是一样的,也就是STRCF保留了SRDCF的空间正则来抑制边界效应;在第一项中,STRCF没有对过去的每一帧进行求和来训练,这就减小了计算量;同时STRCF加入了第三项时间正则,使得新得到的滤波器与之前的滤波器之间的变化尽可能小,相当于保留了之前的信息。
这么做有两点好处:首先,STRCF可以看作SRDCF的一个合理近似,能很好地发挥后者同样的作用;此外,由于时间正则的引入,使得STRCF不易于在当前帧上过拟合,在遇到遮挡或者超出画面等问题时,STRCF能很好地保持与之前滤波器的相似度从而降低了跟踪器完全跟丢到另一个物体上去的可能,这一定程度上提高了STRCF的精度。

此外,ADMM算法的引入使得最优化求解问题有了闭式解,这比Gauss-Seidel迭代法用稀疏矩阵求解要快得多。得益于SRDCF的凸性,ADMM也能收敛到全局最优点。

UPDT

UPDT区别对待深度特征和浅层特征,主要考虑的是缺少数据和深层卷积在增加语义的同时降低分辨率这两个问题。作者分析了数据增强(flip,rotation,shift,blur,dropout)和鲁棒性训练(也就是tracker应对各种复杂场景和恢复的能力,可以通过扩大正样本的采样范围来训练)对deep feature和shallow feature分别的影响,发现deep feature能通过数据增强来提升效果,同时deep feature主打的是鲁棒性而不是精度;相反,shallow feature经数据增强后反而降低了效果,但同时它能够很好地保证精度。因此,作者得出了深度模型和浅层模型应该独立训练,最后再融合的方案。
作者在文中还定义了Prediction Quality Measure,考虑了精度和鲁棒性,精度用响应分数的锋利程度(sharpness)来体现,鲁棒性则用响应值的幅度来表示,幅度越高表明tracker越确信跟踪的目标,也就是鲁棒性越高。关于具体公式的推导和分析,以及Prediction Quality Measure在预测过程中的具体使用可以看一看原文。

ACT

ACT使用了强化学习,构建了由Actor和Critic组成的学习框架。离线训练时,通过Critic指导Actor进行强化学习;在线跟踪时,使用Actor来定位,Critic进行验证使得tracker更加鲁棒。不同于之前的搜索方案(随机采样或者通过一系列分离的action来定位),ACT希望的是搜索一步到位。这步最优的action也就是离线强化学习所关注的行动,而强化学习的状态由输入到网络中bounding box中框出的图片定义,奖励值根据IoU来定义。
在训练的过程中,由于action space比较大,因此要获得一个正奖励比较困难(随机采取action的话IoU恰好高于阈值的可能性较小)。因此作者利用了第一帧的信息来初始化Actor以适应新的环境。同样的,由于巨大的action space,原本DDPG方法中的噪声引入就不适合跟踪任务了,因此在训练前期,Actor采取的行动以某种概率被一种专家决策所替代。随着训练的进行,Actor越来越强大,这时就逐渐减弱专家决策的指导作用。
在跟踪的初始帧,作者首先在第一帧提供的ground truth周围采集多个样本。然后使用Actor对这些样本作action,根据得分对Actor进行一次微调;对Critic,根据打分和ground truth也进行一次初始化训练。
在之后的跟踪过程中,若Critic的给分大于0,则采用Actor的输出一步到位地预测下一帧的目标;否则,再使用Critic在上一帧周围采集的样本中选出最优作为目标,完成重定向。此外,可以认为Actor在离线训练时已经比较稳定了,因此在跟踪过程中只对Critic进行更新,且仅在Critic给分小于0(认为Critic没能很好地适应目标的变化)时,取前十帧的样本来更新Critic。

DRT

DRT引入了可靠性的概念,考虑到空间正则、掩模等抑制边界效应的方法都不能抑制bounding box内部的背景信息,同时这些方法会导致滤波器的权重倾向于集中在某些较小的区域(主要是中央的关键区域,我理解为边缘区域被抑制掉了,因此学习时自然不会去分配权重),作者认为这是不利于目标跟踪的(容易个别不可靠的区域被误导)。为此,作者提出了DRT,它主要是将滤波器分成了一个base filter和一个reliability term的element-wise product:

这里的base filter用于区分目标和背景;reliability term用于决定每片区域的reliability,由目标区域每一个patch的reliability值加权求和决定:

这里的$p$是对每一个patch的掩模,用于确定每个patch做相关操作的区域;$\beta$有上下界的限定,目的就是为了降低feature map中响应不平衡的影响,防止由于响应的集中而导致仅有一小块区域被关注。

需要最小化的目标方程包括三项:分类误差、局部一致性约束和滤波器参数$h$的二范数。分类误差就是与ground truth之间的损失函数,计算时需考虑可靠性;局部一致性约束用于减小循环样本中的每一个片段的响应差距,该项不受$\beta$即可靠性的影响,也就是说base filter在训练时依旧要保持对每个局部区域同样的关注度,使得base filter能独立于可靠性进行训练,这就避免了前面提到的滤波器在训练时边缘区域被抑制所造成权重集中的后果;滤波器参数$h$的二范数用于保证模型的简单程度,防止模型退化(可以理解为过拟合)。
由于只有当base filter的参数$h$和reliability的权重$\beta$有一项已知时,目标方程的最小化问题才是凸优化问题,因此作者采用了$h$、$\beta$交替训练的方法。
作者还使用权重逐帧退化的方式设计了一种简单的利用多帧信息的目标方程。借鉴ECO,DRT也采用了间隔几帧更新一次的稀疏更新方法和基于高斯混合模型的样本分组策略。类似DSST,DRT采用了先确定位置再计算多个尺度的响应的“两步”尺度估计方法。

MCCT

MCCT使用了多特征集成学习,在跟踪时对每一帧分别选用最合适的特征来做出决策。为了应对不同的场景,MCCT选择了low,middle,high三个层级的特征,并通过排列组合得出7种expert。尽管有些特征的鲁棒性明显差于三类特征的组合,但是它们提供的多样性对集成学习是至关重要的。

为了评估每个expert在每一帧的好坏以决定具体选用哪一个,作者提出了Expert Pair-Evaluation和Expert Self-Evaluation。
Expert Pair-Evaluation分为两项:在第一项中,作者认为一个expert的好坏可以通过它与其他expert的整体一致性来体现,于是首先计算了每个expert相对于其他6个expert在当前帧预测结果的一致性(通过重叠率来衡量)之和;此外,作者认为一个好的expert还必须是temporal stable的,因此他又计算了每个expert相对于其他6个expert在前几帧内预测趋势的一致性,这就可以防止因为在当前帧碰巧预测一致而导致之前一项的分值很好的情况,也保证了expert的可信度。最后两项结合得到Expert Pair-Evaluation。
在Expert Self-Evaluation中,作者认为路径的顺滑程度一定程度上能够体现每个expert的可靠程度。
最后将Expert Pair-Evaluation和Expert Self-Evaluation加权求和选出每帧最好的expert做出决策。
MCCT提出了一种peak-to-sidelobe ratio和鲁棒性的置信度分数来进行模型更新:

其中,$P_{mean}^{t}$是每个expert响应图peak-to-sidelobe ratio的平均,$R_{mean}^{t}$亦然。当$R_{mean}^{t}$比较低时,认为采集到了不可靠的样本(比如遮挡问题等)。为此,作者的模型更新策略是,当置信度分数$S^{t}$大于之前置信度均值时,采用正常学习率,否则,根据置信度算出一个较小的学习率以在一定程度上维持模型。
为了提升速度,每个expert之间共享了样本和RoI,最后MCCT的速度为7.8FPS,MCCT-H(没采用深度特征)的速度为44.8FPS。(作为参考,ECO的速度为15FPS)

LSART

LSART分析了深度特征中的空间信息,提出了两种互补的回归方式来使得跟踪更加鲁棒。
作者首先对比了CNN-based和KRR-based(核岭回归)两类tracker,认为它们各有利弊且是互补的。由于KRR的循环采样,目标的结构特征会被打破,对形变和遮挡问题效果不好,而CNN则能够很好地提取位置信息;相反,CNN庞大的参数量使得它容易过拟合,而KRR-based tracker就不会出现这样的问题。因此,若将两者结合(将热力图加权求和),就可以让KRR关注全局而让CNN关注较小、较精确的目标,进而达到更好的效果。
对于KRR,作者引入cross-patch similarity,将参数看作训练样本的加权求和,将响应项拆分成三个模块,这就方便把原本的迭代求解的方式分成三步在神经网络中来求解了。
对于CNN,考虑到形变和遮挡等问题会使得目标的一部分比其他区域更加重要,不同于以往在feature map上做文章,作者对卷积层的滤波器施加掩模,使得各个滤波器关注于不同的区域,在跟踪的过程中,这些掩模不做变化。此外,作者还提出了距离变换池化层用于评判输入feature map的可靠性。另外,作者设计了一种two-stream的训练网络,将空间正则的卷积层和距离变换池化层分开训练以防止过拟合,能够比较好的处理旋转问题。

SiamRPN

SiamRPN利用了Faster RCNN中的RPN,解决了之前深度学习跟踪算法没有domain specific(可理解为类间不区分)以及还需额外的尺度检测与在线微调的问题。RPN回归网络的引入,一方面提高了精度,另一方面回归过程代替多尺度检测,使得速度有所提升。
在在线跟踪时,SiamRPN将跟踪看作one-shot检测的问题,也就是用第一帧目标样本的信息来预测RPN网络中的参数,从而实现domain specific且不需要在线更新。作者把template分支和detection分支卷积视作类别信息在RPN网络上的embedding。

DaSiamRPN

DaSiamRPN在之前的孪生网络系列的基础上增加了distractor-aware,这里的distractor指的是在判别式方法中,不同于无语义信息易判别的背景,而存在一定的语义并对前景分割存在干扰的背景。这其中的一大原因是之前的训练集仅从同一个视频序列的不同帧中采样,造成了non-semantic的背景样本具有较大的比重而semantic的背景样本较少,这就弱化了模型准确判别前景的能力。此外,之前的孪生网络系列还存在不能在线更新和不进行全局搜索这两个问题。
首先,作者提出了三类样本选取方法来弥补传统采样的不足。考虑到视频数据集中类别缺乏和标注的难度,作者引入了ImageNet和COCO图像检测两个数据集,并把样本分成三类对tracker进行训练。

对于正样本对,其作用是提升tracker的泛化能力和回归精度;对于来自同一类别的样本对,其作用是让tracker更注重细粒度的表达方式,提升判别能力;对于来自不同类别的样本对,其作用是让tracker在遮挡、超出视野等情况下拥有更好的鲁棒性。
值得一提,作者发现motion pattern能很好地被浅层网络建模,因此在数据增强时还引入了运动模糊。
DaSiamRPN通过上述方法对数据做了增强,可是在跟踪特定目标时,还是很难将一般模型转化为特定视频域所用。考虑到上下文信息和时域信息可以提供特定目标的信息以增加tracker的判别能力,作者提出了一个distractor-aware module。具体来说,在上一帧中选择出的proposal中,通过非极大抑制处理,剩下的proposal中最大的就是目标,剩下的就是会产生误导的distractor;在当前帧,为了抑制这些distractor的干扰,可以减去这些distractor之前响应的加权和,减去之后还是最大的proposal就是我们要找的目标,其基本思想如下公式所示:

这里的$\alpha$是控制distractor影响大小的权重系数,作者又对上式进行调整,通过引入学习率使得该分类器在线可学习,这就无需利用反向传播更新网络参数,而通过微调一个分类器弥补了传统基于孪生网络的tracker不能在线更新的缺点。
此外,当认为目标跟丢时,DaSiamRPN会匀速扩大搜索范围,并且通过高效的bounding box回归来代替图像金字塔,这就通过一个简单的方法在应对长时跟踪目标消失问题时较之前基于孪生网络的tracker取得了一个进步。

Meta-Tracker

Meta-Tracker将元学习运用在了目标模型的初始化上。作者认为结合深度特征和在线学习的模型有两大困难,一是训练的样本不容易获得,二是大多数SOTA的tracker在训练阶段都需要花费大量的时间在初始化上面。
针对上面的难题,作者提出了一种在未来的帧上训练目标模型的思路,采用了基于预测梯度的策略学习方法获得普适性的初始化模型,使得跟踪模型自适应于后续帧特征的最佳梯度方向,从而在接收到第一帧时仅需一步迭代就能使参数快速收敛到合适的位置。这样做有三点好处,一是能使模型更加关注对后续的帧更有价值的特征,二是避免了在当前帧上过拟合,三是能够使初始化更快速。总而言之,就是能保证精度和鲁棒性。
考虑到上述方法在长序列或者目标在帧与帧之间变化不大时表现不佳(会偏离目标),这是因为Meta-Tracker一步到位的思想使得学习率会偏大。因此作者仅在模型初始化时采用学习到的学习率,在之后的跟踪过程中仍旧沿用原来版本的方式进行更新。这里原来的版本指的是CREST和MDNet,作者在这两个tracker的基础上改进出了MetaCREST和MetaSDNet。具体的改进和处理可以看一看我之前写过关于Meta-Tracker的文章。

DorT

DorT(Detect or Track)把跟踪看作一个连续决策的过程,它结合目标检测和目标跟踪两个领域内SOTA的结果,在孪生网络输出的结果上再添加一个小型的CNN网络作为scheduler来判断在下一帧是作检测还是作跟踪。

LADCF

LADCF针对DCF系列的边界效应和模型退化(后者主要是单帧独立学习和模型更新速率固定导致)的问题,提出了一种空间域特征选择和时间域约束结合的方法,并且使其能在低维流形中有效表示。

补充:流形学习的观点认为,我们所能观察到的数据实际上是由一个低维流形映射到高维空间上的。由于数据内部特征的限制,一些高维中的数据会产生维度上的冗余,实际上只需要比较低的维度就能唯一地表示。

掩模策略应用于目标跟踪时,仅将目标区域的参数激活。LADCF也运用了这个思想,对滤波器中的参数$\theta$作降维处理$\theta _{\phi }=diag(\phi )\theta$,这里$\phi$中的元素要么是0、要么是1,即不激活或者激活。不同于PCA和LLE,这种方法在降维的同时也保持了空间特性,不仅能加速求解,也能除去大部分干扰,使滤波器关注于目标部分从而可以使用更大的搜索域。
最后的目标函数如下:

可以看到,这里还包括与历史模型的正则项,减轻了滤波器退化。作者让$\lambda _{1}< < \lambda _{2}$,也就是让时间域上的一致性更加重要于特征的稀疏选取。

FlowTrack

不同于之前先把光流算好,FlowTrack是第一个把光流信息进行端到端训练的,这无疑提高了光流使用的精度。作者注意到之前的算法大都采用RGB特征也就是外观特征,且缺少对运动特征和帧与帧之间联系的利用,这就导致在部分遮挡和形变等情况下效果会变差。不同于把之前的几帧保留下来等时域方法,作者希望能把前几帧的特征直接补充到当前帧上融合成一个,而具备方向和速度信息的光流就成了一种比较合适的方法。为此,作者将光流引入孪生网络框架,使得仅使用外观特征的一些不足得到弥补。
作者的思想很巧妙,我以遮挡问题为例简述一下。作者的想法是当当前帧的目标有部分被遮挡时,我们可以根据光流的运动信息,把前几帧的特征映射过来对齐,通过插值补全当前帧的特征,简单来说可以理解为把当前帧缺的那块给补全。为了有效地选择补过来的特征,作者使用了空间注意力和时间注意力两种机制结合。空间注意力分别计算前几帧补过来的特征与当前帧的特征的相似度,根据相似度大小来分配特征在各个空间位置上的权重。但是由于最近的一帧总是与当前帧的特征最为相似,它的权重肯定是最大的,考虑到假如最近一帧由于遮挡等原因等导致特征质量下降而不适合分配最大的权重,因此还需要时间注意力来重新校准权重。具体来说,时间注意力作用在空间注意力的输出上,对于一般情况下的目标基本不会改变空间注意力的输出,而对遮挡等情况下的帧就会减小其在空间注意力中分配到的权重。

VITAL

VITAL针对正样本高度重叠无法捕获目标丰富的特征变化和正负样本不平衡的问题,采用对抗学习的思想,分别设计了生成网络和代价敏感损失来解决这两个问题。
这里的生成网络输出为一个作用于目标特征的掩模。通过对抗学习,该生成网络可以产生能保留目标特征中较为鲁棒的部分。其目的是对仅在个别帧判别力强的特征进行削弱,防止判别器过拟合于某个样本。个人理解,这里的判别力强的特征并不是指的能够很好地区分目标和背景的特征,而是指仅在某些帧出现比较独特,而在大多数情况下不存在于目标上的特征。
考虑到很容易被分类正确的负样本在训练过程中也会产生损失,然而我们并不希望网络关注这些损失,因为关注他们反而会使得网络性能变差。因此,为了避免很多负样本主导损失函数,作者采用高阶敏感损失,减小简单负样本的权重,这不但提升了精度,也加速了网络的收敛。

SA-Siam

SA-Siam考虑到语义信息和外观信息的互补关系,构建了两个并行的孪生网络。在外观网络(作者使用的是SiamFC)具有判别力的基础上,结合语义网络(作者使用了AlexNet的第4和第5层作为backbone)更泛化、更鲁棒的语义信息,使得tracker的效果更好。
外观网络的输入为目标和搜索区域,其框架跟SiamFC基本一致,这里就不细说了。
不同于外观网络,语义网络的输入为目标及其背景和搜索区域(两者一样大),作者直接使用了AlexNet提取高层的语义信息并不进行训练,因为假如训练了的话就跟外观网络接近了,而集成学习的思想就是被集成的各个模型之间的关联性应比较弱,这也是文中多次强调的。在提取高维语义信息之后,为了使得得到的语义特征更适合于跟踪任务,作者使用了1x1的卷积层作了一次特征的fusion。此外,考虑到对不同的追踪目标各个通道的重要性是不同的,作者还设计了一个通道注意力模块,具体而言,就是对每一个通道,先做最大池化变成3x3的网格,然后再通过一个多层感知机来决定每个通道的每个格子的重要性。由于通道注意力的选择也受目标周围背景的影响,因此最大池化的结果仅中间的格子是目标区域,周围的8个格子代表目标周围的背景信息。再次强调,在训练过程中,语义网络冻结语义特征提取的AlexNet,仅训练特征fusion和通道注意力模块。
根据上文的解释,同样的,为了保持语义网络和外观网络的弱关联性和互补性,两支网络独立训练,仅在测试时加权得到最后的相似度图作为预测依据。

2019

下面是VOT2019 short-term的排名结果。

GFS-DCF

GFS-DCF考虑到深度网络的高维通道存在许多冗余的信息,因此作者在时域和空间域之外,还考虑了通道维度的影响,在目标函数中使用了三个正则项。

注:建议结合上图来看接下来的分析。

对于空间域,作者将每个通道(也就是每个feature map)的对应点相连接,用范数约束,可以理解为提取那些在绝大多数特征图中都是最重要的特征的位置。

从通道角度,作者又把每一个通道作为一项来做约束,可以理解为提取那些特征比较重要的通道。

对于时域,作者使用了low-rank约束,这里的rank指的是矩阵的秩而不是排名,low-rank主要用于图像对齐(alignment),在文中的目标是最小化$rank(W_{t})-rank(W_{t-1})$,这里的$W_{t}$表示从1到t每个滤波器向量化后形成的矩阵。下面是作者最后修改后的正则项:

注:上述三项在实际目标函数中还要加上权重。

作者发现,空间正则对使用handcrafted特征的模型效果显著,而对使用CNN的模型(文中是ResNet)效果提升不大;相反,通道正则对使用handcrafted特征的模型效果不明显,而对使用CNN的模型效果显著。作者在文中解释认为由于深层CNN特征表示的语义信息丰富而缺少细粒度的信息,因此相比保留更多空间结构handcrafted特征,对深层CNN特征使用空间正则比较难以判别哪些位置的特征反应了目标位置的信息。此外,由于在训练过程中一些通道的权重下降到很小,也就是说模型本身就不怎么关注这些通道,因此使用通道正则在这里取得了比较明显的效果。

D3S

D3S考虑到BBox对目标的粗糙表示会影响性能以及视频分割任务中对背景干扰和长时视频不鲁棒的问题,提出了一种视频跟踪、视频分割互补的框架。

如上图所示,作者构造了用于分割的GIM和用于定位的GEM两个模块。
GIM使用初始帧的目标像素点构造目标的特征向量,使用初始帧目标周围的像素点构造背景的特征向量。在之后的图像中,每个像素点的目标相似度,定义为该点和每个目标的特征向量做相似度计算之后,最大的K个目标相似度的均值;同理,每个像素点的背景相似度,定义为该点和每个背景的特征向量做相似度计算之后,最大的K个背景相似度的均值。最后将目标相似度图和背景相似度图作softmax得到分割结果。该模块没考虑位置信息,用分割使得在应对剧烈形变的目标时能够取得较好的效果。
然而,GIM的分割结果对相似的物体或者背景的干扰并不鲁棒,因此我们希望能有鲁棒的位置信息来提升判别力。GEM就是直接使用一个DCF来找到最大响应值的位置,也就是目标的中心位置,然后以该中心为圆心,向周围每个像素点根据半径由大到小分配置信度。最后仅把GIM中得到的且由GEM分配的置信度高于阈值的目标像素点作为分割结果。
由于通过backbone的encode导致此时的输出分辨率较低,因此还需要作上采样。具体来讲,就是每次把输入扩大到两倍分辨率,通过两次卷积,然后加上backbone中对应分辨率的层。
由于预训练的backbone特征缺少精细的划分,因此作者在初始帧先进行降维的训练。具体来讲,就是首先将预训练网络通过1x1的卷积来降维,再通过一层3x3的卷积,从而达到调整网络参数使得分支划分得到最佳的目的。
由于输出是二值掩模,因此作者还对使用BBox的目标跟踪问题作了变换处理。首先,在初始帧,除了进行降维和DCF的训练,作者还先把BBox内部的像素点视为目标点,把BBox外4倍区域的像素点视为背景点,用D3S在第一帧上迭代(文中说只迭代1次就可以了)以产生比BBox更细致的分割,将最后分割出的目标点和其周围的背景点用于构造特征向量,在之后的跟踪过程中保持不变并用于GIM模块。此外,在输出时,作者先用椭圆去近似掩模,然后根据长轴和短轴来确定BBox。由于椭圆的确定遗漏的背景信息(仅考虑怎么把mask包进来)从而导致BBox偏大,因此作者提出了一种考虑背景信息的方法,对长轴进行微调来确定最终的BBox。

GlobalTrack

GlobalTrack关注的是long-term tracking的问题。我们知道,在目标跟踪问题中,为了更好的利用前一帧甚至前几帧的信息,往往会对模型做很多假设,包括目标的运动、位置变化、尺度变化(假设平滑变化等等),而这些假设并不能很好地处理所有的情况(比如位置或尺度突变、目标消失、短时跟踪失败等),由此产生了模型的累计误差。而在长时跟踪问题中,这样的累计误差往往会使得后期的目标跟踪结果差很多。
基于上述考虑,GlobalTrack根据长时跟踪的特点,把跟踪看作在每一帧作全局检测的问题,设计了一种没有运动模型、没有在线学习、没有位置估计、没有尺度平滑的无累积误差的baseline,其基本框架如下图所示。

受Faster RCNN启发,GlobalTrack也是基于two-stage的框架。其下面的一条和Faster RCNN基本一致,由于Faster RCNN是目标检测类算法,其目的是在图像中框出所有物体并分类。而目标跟踪仅需要目标,因此作者用添加了上面一条query-specific的引导。为了简单起见,作者把query的RoI看成kxk的方型特征,在第一个feature modulation中,作者把RoI特征卷积成1x1的卷积核,然后再和通过backbone的搜索图像特征做卷积相关操作,得到query-specific的候选框;在第二个feature modulation,作者把RoI特征与每个候选框作哈达玛积(也就是简单地将两个尺寸相同的矩阵的对应位置作乘积),由此来改进标签置信度和BBox的预测。
在训练时,作者取多对图像对,其中每一对图像都共同含有M个相同的实例,作者用每对图像来相互预测每对各自的M个实例,使总的损失达到最小以进行训练。
在测试时,作者简单地把第一帧作为query,之后的每一帧都视为独立的全局检测,直接取得分最高的BBox作为结果,如此就不存在依赖相邻帧带来的累计误差了,因此作者认为视频长度越长,GlobalTrack的表现就越突出。

SPSTracker

SPSTracker针对目标周围噪声引起的响应(sub-peak)导致模型漂移,以及由多尺度样本加权得到的特征图响应最大值和目标真正的几何中心不一致的问题,提出了BRT和PRP两个模块。作者使用的是ATOM的框架,两个模块在框架中的使用方式如下图所示。

这里的BRT其实就是简单的将远离目标中心的点置为0,其目的是减小响应图的方差,使得响应图尽可能地呈现单峰响应的形状,此外也起到了抑制边界效应的效果。PRP其实就是作者新设计的一个池化层,该池化层把每一像素点的值设为该像素点所在列所有像素点中的最大值和所在行所有像素点的最大值之和,从而使响应值能更加靠近目标的几何中心,从而能够更好地使用多尺度样本。作者对搜索区域使用BRT,然后将通过分类器的置信度图通过一个PRP构造的残差模块,从而达到抑制sub-peak的目的。

SiamRCNN

SiamRCNN发现重检测很容易受到干扰物的影响从而产生模型漂移,从难例挖掘和运动轨迹动态规划两个角度入手,设计了一个利用第一帧和前一帧为模板的孪生网络检测结构,在短时跟踪评价上能与之前SOTA的算法持平,在长时跟踪评价上有非常显著的进步。注意,这里的追踪轨迹不仅追踪目标,同时也追踪所有的distractor。

SiamRCNN可以说综合了许多前人的成果。为了收集更多难例,作者在不同的视频上根据嵌入网络上的距离来获得更多负样本。接下来,我结合上面的网络结构图来简述一下我对该算法的理解。
首先,作者使用RPN网络获得了类别无关的proposal,然后和第一帧结合,通过一个与Faster RCNN前半段相似的Re-Detection Head。接着,通过3个级联的RCNN获得回归后的bounding box(这里借鉴了Cascade RCNN,其认为高质量的proposal能够产生更好的结果,3级RCNN的IoU阈值设定逐级升高)。随后,作者将此时获得的bounding box与前一帧所有的检测结果两两组合(也就是即检测目标也检测干扰物),再次输入到之前相同的重检测结构中。最后对得到的所有结果进行跟踪轨迹动态规划。
在轨迹动态规划模块,作者计算轨迹与检测结果的相似度,用不具有不确定性的检测结果去延续之前的若干条轨迹。这里的不确定性定义为:对这条跟踪轨迹,有其他的检测结果和本结果具有相当的相似度;对检测结果,有其他的跟踪轨迹和本轨迹具有相当的相似度。对于不确定的检测结果,作者让它们开启新的轨迹。对于没有确定检测结果的轨迹则暂时中断,其中也包括含有初始帧的轨迹,当它中断时可认为目标消失了。


研究趋势

以下是我对近几年来目标跟踪领域各种算法主流的研究趋势和发展方向的一个浅析,个人思考,多多指教。

注:其实近几年还出现了一些其他的关注方向,由于不是主流、目前关注较少、本人学识不够等原因,在此不做列举。

信息提取

深度特征

早期的目标跟踪算法主要在handcrafted特征方面进行探索和改进,以2012年AlexNet问世为节点,深度特征开始被引入目标跟踪领域。
我们知道,在现实场景中,物体是在三维的运动场中移动的。而视频或图像序列都是二维的信息,这其实是一些难题的根本原因之一。一个比较极端的例子就是理发店门前经常会出现的旋转柱,如果单纯地从二维角度来看,柱子是向上运动的,可在实际的运动场中柱子是横向运动的,观测和实际的运动方向是完全垂直的。

因此,为了能够更好地跟踪目标,我们需要提取尽可能好的特征,此外最好能从视频或图像序列中学到更多丰富的信息(尤其是含语义的)。值得注意的一点是,在港科大王乃岩博士2015年所做的ablation experiments中(详见参考文献[2]),发现特征提取是影响tracker效果最重要的因素。
考虑到精度是保证目标跟踪鲁棒性的重要因素,不同于一些其他的计算机视觉任务,目标跟踪领域的深度算法比较强调结合与充分利用浅层网络的高分辨率信息。

时域和空间域结合

可以说,在目标跟踪的相关算法中,空间正则指的就是抑制边界效应。由于CNN能够在学习的过程中能够产生对样本中各个区域有区分的关注度,因此可以不考虑边界效应。对边界效应的处理主要是在相关滤波类等需要循环移位的算法中出现。
事实上,目标跟踪这一个任务本身就在利用时域信息,因为预测下一帧肯定需要上一帧的信息,然而仅仅利用上一帧的信息往往是不够的,充分的利用时域信息在正则或者辅助记忆方面都可以取得一定的效果。

学习方式

结合语义分割的多任务学习

由于bounding box粗糙的对目标的标注表示使得有不少冗余的背景信息进入template,此外bounding box对平面内旋转等场景不鲁棒,这些都会导致模型的退化。早期也有许多算法考虑到了分割,但主要是对目标进行分块(part-based)而不是语义分割。由于跟踪的目标是有具体形状的且是一起运动的,同时判别式方法正是要分离除目标以外的物体,因此近年来有不少算法结合语义分割,通过多任务学习来进一步提高tracker的效果。具体来说,就是引入掩模(mask)来识别预测物体,最后在转化成bounding box作为结果。

元学习

实际上,目标跟踪这一个任务本身的特性就决定了它与元学习的思想有共通之处。元学习主要针对的是两个问题:在少样本学习的情况下对样本的利用效率比较低;当进行一个新的任务时对之前学到的经验的可移植性差,我觉得这里的新任务可以指从分类到跟踪这样类别之间的转换,也可以指在不同的视频序列上训练和测试这样“域”之间的转换。
当深度特征兴起之后,目标跟踪中的许多算法都选择迁移目标分类任务中的一些预训练模型来提取特征,这种迁移学习其实就包含了元学习的思想。MDNet将每个视频看做一个域,在测试时新建一个域但同时保留了之前训练时在其他域上学到的经验,既能够更快更好地在新的视频序列上学习也避免了过拟合。孪生网络实际上也是元学习领域一种比较常用的结构,它学习了如何去学习输入之间的相似度。

其他关注点

样本采集

样本采集主要包括样本的数量、样本的有效性、正负样本和难易样本的平衡性。

防止过拟合

每帧获取的少量信息和目标的意外变化导致信息的丢失,使得过拟合问题成为目标跟踪任务中一个比较重要的关注点,下面是一些比较常见的方法:

]]>
+

看了一寒假的目标跟踪,一直想将自己学习到的内容整理归纳一下,迟迟没动笔(其实是打字,但说迟迟没打字比较难听哈哈)。如今快要开学了,决定还是积累一下。
本文主要是把目标跟踪中的一些相关要点做一个总结,并且按具体问题对一些主流的或者我阅读过觉得有价值的算法做一个简单概述。近年来各类方法层出不穷,且码字不易,无法涵盖所有的方法。此外,由于包含自己的转述和理解可能会存在错误。在今后的学习过程中会一直保持本文的更新,因此这将是一篇LTS的文章哈哈。

注:本文重点关注单目标跟踪。

References

参考文献:
[1]统计学习方法(第2版)
[2]Understanding and Diagnosing Visual Tracking Systems
[3]Survey of Visual Object Tracking Algorithms Based on Deep Learning
[4]Handcrafted and Deep Trackers: Recent Visual Object Tracking Approaches and Trends
[5]Review of visual object tracking technology
[6]A Review of Visual Trackers and Analysis of its Application to Mobile Robot
[7]Deep Learning for Visual Tracking: A Comprehensive Survey
[8]Video Object Segmentation and Tracking: A Survey
[9]Object Tracking Benchmark
[10]The Visual Object Tracking VOT2013 challenge results
[11]The Visual Object Tracking VOT2014 challenge results
[12]The Visual Object Tracking VOT2015 challenge results
[13]The Visual Object Tracking VOT2016 challenge results
[14]The Visual Object Tracking VOT2017 challenge results
[15]The sixth Visual Object Tracking VOT2018 challenge results
[16]The Seventh Visual Object Tracking VOT2019 Challenge Results

注:参考文献重新整理中,待补全…


简介与要求

目标跟踪是利用一个视频或图像序列的上下文信息,对目标的外观和运动信息进行建模,从而对目标运动状态进行预测并标定目标位置的一种技术。一般是在第一帧给出一个框,框中的物体就是我们需要在后续帧中用算法进行跟踪的对象。就目前的单目标跟踪而言,一般有如下要求:
monocular:我们的视频或者图片序列是仅从一个摄像头中获得的,也就是不考虑比如在城市道路场景中跨摄像头对目标跟踪的复杂应用。
model-free:没有任何先验,也就是在获取第一帧的框之前我们并不知道会框出什么物体,也不需要在之前对初始框中的物体进行建模。
single-target:只追踪第一帧框出的那一个物体,也就是除了那个物体之外所有的物体都是back ground。
casual/real-time:目标跟踪是一个在线过程,也就是不能提前获取未来的框对目标进行跟踪。
short-term:没有重检测,也就是目标跟丢了就丢了。
long-term:可以在跟丢之后重检测,这类算法一般除了跟踪之外还需要有检测的功能。

下面是目标跟踪流程的伪代码表示(不一定普适,比如有些算法不在线更新,但符合基本的过程)。


问题及挑战

通俗来讲,目标跟踪的最终目标就是要又快又准。“快”主要表现在计算量小和所需的存储空间小,“准”就是预测出的bounding box要尽可能地接近ground truth。除了上面两个基本需求(也可以说是为了更好地达到这两个基本需求),近年来的算法主要针对目标跟踪中的一些挑战进行突破,从而更好地解决某些问题之后达到更好的整体效果。
总的来说,目标跟踪的主要问题有如下这些:遮挡(occlusion)、背景干扰(background clutter)、光照变化(illumination changes)、尺度变化(scale variation)、低分辨率(low resolution)、快速移动(fast motion)、超出画面(out of view)、运动模糊(motion blur)、形变(deformation)、旋转(rotation)等。

OTB数据集依据各种问题对其中的序列进行了一个划分,这对之后针对性的研究提供了重要的参考。


生成式与判别式

利用特征判断候选样本是否为跟踪目标,可将目标跟踪的模型分为生成式模型和判别式模型,本小节就介绍一下什么是生成式模型和判别式模型。

机器学习

我们首先看看在机器学习中生成式模型和判别式模型定义的一般区分。
一般而言,机器学习的任务就是学习一个模型,应用这一个模型,对给定的输入预测相应的输出。输出的一般形式可以是决策函数,也可以是条件概率分布。
对于生成式模型,我们需要通过数据学习输入X与输出Y之间的生成关系(比如联合概率分布),也就是认为X和Y都是随机变量。典型的生成式模型有朴素贝叶斯模型、隐马尔可夫模型(HMM)、高斯混合模型(GMM)等。
对于判别式模型,我们只需要直接学习决策函数或者条件概率分布,只关心对给定的输入X我们需要输出怎么样的Y,也就是不考虑X是否是随机变量。典型的判别式模型包括k近邻、感知机、决策树、逻辑斯蒂回归模型、最大熵模型、支持向量机(SVM)、提升方法和条件随机场等。此外神经网络也属于判别式模型。
相较而言,生成式模型体现了更多的信息,不过这还是因条件而异的,不同情况不同任务两种方法各有优缺点。

目标跟踪

在目标跟踪领域,生成式模型通过提取目标特征来构建表观模型,然后在图像中搜索与模型最匹配的区域作为跟踪结果。不论采用全局特征还是局部特征,生成式模型的本质是在目标表示的高维空间中,找到与目标模型最相邻的候选目标作为当前估计。此类方法的缺陷在于只关注目标信息,而忽略了背景信息。

与生成式模型不同的是,判别式模型同时考虑了目标和背景信息。它将跟踪问题看做二分类或者回归问题,其目的是寻找一个判别函数,将目标从背景中分离出来,从而实现对目标的跟踪。

一般来说,在目标跟踪领域,判别式充分利用了目标前景和背景信息,能更加有效地区分出目标,比单单运用目标区域特征进行模板匹配的生成式模型在复杂环境中的鲁棒性更强。


算法导图

首先是参考文献[6]中的一个树状导图。

下图是中科院博士王强(github名为foolwood…呃不得不说这名字取得真谦虚)在github上上总结的Benchmark Results中的一个思维导图,同一个链接下还包括了各项成果的paper及code,值得收藏一下。

补充:这里再推荐一个在github上维护的Tracking Benchmark for Correlation Filters,按每篇论文针对或者解决的问题来分类,比较清楚,可以收藏一下。但这个仓库似乎在2017年后就没有更新了,可能是深度学习的进入或者说相关滤波系列和深度学习融合使得独立的相关滤波算法不那么突出了。

下图是浙大硕士王蒙蒙极市平台做分享的时候所用的一张思维导图,归纳得也比较清晰。

注:后两张导图中都把历年benchmark的冠军工作作了标注。

对比几张思维导图可以发现,他们都把主流算法分成了相关滤波、深度学习两个分支(或者说是基于handcrafted特征的算法和基于CNN提取特征的算法,其实近年已有所融合),此外还有一些基于强化学习、结构化SVM的模型。其实,目标跟踪算得上是计算机视觉领域中深度学习涉足较晚的一个方向,其主要原因是目标跟踪相关数据集的标注花费较大。此外,相关滤波的速度优势,也就是实时性是十分引人注目的,但在应付当前目标跟踪中的各种挑战、问题时,相关滤波的鲁棒性还是落后于深度学习方法的。
在下一节,我将结合上面几张导图,对历年尤其是近几年的算法做一个简单的整理,以方便日后的学习与研究。


各类算法的梳理与简述

本节按年份顺序对各个算法进行一个简单地梳理,其中各个算法的年份以论文发表的年份或者参加benchmark的年份为依据,可能会存在1年的区别,但影响不大。其中各年的各个算法根据算法的效果和影响大致上呈递减排序。对2013以后的算法,我拷贝了VOT challenge的结果排名,以供参照。

注意:如果你对计算机视觉或者说目标跟踪方面的一些基础方法、概念和经典算法已经有些了解,可以跳过本条建议。
考虑到在后文频繁地插入链接不太好,我就在此先推荐一下我博客的几个标签目标跟踪计算机视觉深度学习机器学习以及线性代数,其中的文章包含了一部分接下来要提到的概念和算法,可以事先浏览一下。当你在阅读时对相关概念、方法感到迷惑或者想进一步了解,博客内置的搜索功能或许能够为你提供帮助。

1981

LK Tracker

LK Tracker应该是最早的目标跟踪工作,它使用了光流的概念,如下图所示,不同颜色表示光流不同的方向,颜色的深浅表示运动的速度。

LK Tracker假定目标灰度在短时间内保持不变,同时目标邻域内的速度向量场变化缓慢。由于光流方程包含坐标x,y和时间t共三个未知数,其中时间变化dt已知而坐标变化dx和dy未知,一个方程两个未知数无法求解,因此作者假定相邻的点它们的光流具有空间一致性,即实际场景中邻近的点投影到图像上也是邻近点,且邻近点速度一致,这样就可以求解方程组了。下图是求解之后的光流向量,其中绿色箭头的方向表示运动方向,线段长度表示运动速度的大小。

光流的计算非常简单也非常快,而且由于提出得很早,各种库都有实现好的轮子可以轻松调用,但是它的鲁棒性不好,基本上只能对平移且外观不变的物体进行跟踪。

1994

KLT

KLT是一种生成式方法,也是使用了光流特征。在此基础上,作者使用了匹配角点的方法,也就是寻找边角处、纹理处等易辨识的地方计算光流来进行追踪。

1998

Condensation

Condensation(Conditional density propagation)条件密度传播使用了原始的外观作为主要特征来描述目标,采用了粒子滤波,这是一种非参数化滤波方法,属于生成式模型。它定义了一个粒子样本集,该样本集描述了每个粒子的坐标、运动速度、高和宽、尺度变化等状态;此外,通过一个状态转移矩阵和噪声定义系统状态方程。基于蒙特卡洛方法,粒子滤波将贝叶斯滤波方法中的积分运算转化为粒子采样求样本均值问题,通过对状态空间的粒子的随机采样来近似求解后验概率。

2002

Mean Shift

Mean Shift采用均值漂移作为搜索策略,这是一种无参概率估计方法,该方法利用图像特征直方图构造空间平滑的概率密度函数,通过沿着概率密度函数的梯度方向迭代,搜索函数局部最大值。在当时成为了常用的视觉跟踪系统的目标搜索方法,简单易实现,但鲁棒性较低。

2003

Feature Selection

Feature Selection利用线性判别分析自适应地选择对当前背景和目标最具鉴别性的颜色特征,从而分离出目标。

2006

Boosting

Boosting结合Haar特征和在线Boosting算法对目标进行跟踪。Boosting算法的基本思路就是首先均匀地初始化训练集中各个样本的权重,然后初始化N个弱分类器,通过训练集进行训练。第一次训练时,对第一个弱分类器,通过它在训练集上的错误率确定它的权重,同时更新训练集的样本权重(增加分类错误的样本的权重),然后,用新的训练集训练第二个弱分类器,计算它的权重并更新训练集的权重。如此迭代,将得到的分类器与它们的权重相乘,累加起来便得到一个强分类器。
上面所述是针对离线训练的,当在线训练(比如跟踪)时,为了满足实时性,就必须减少样本数量。Boosting的做法是对每一帧采集的样本仅使用一次便丢弃,然后进入下一帧采用新的样本。
以上就是在线Boosting算法的简单理解,具体而言,Boosting这里选择的弱分类器其实是Haar特征。由于Haar特征其实是一组特征,于是就需要Boosting算法根据每种Haar特征的响应来从Haar特征池中选出一个子集用于构造强分类器。

2008

IVT

IVT渐进地学习一个低维的子空间表示来自适应目标物体的变化,它将以前检测到的目标乘以遗忘因子作为样本在线更新特征空间的基而无需大量的标注样本。

2010

MOSSE

MOSSE(Minimum Output Sum of Squared Error)使用相关滤波来做目标跟踪(不是第一个,但可以看作前期的一个代表),其速度能够达到600多帧每秒,但是效果一般,这主要是因为它只使用了简单的raw pixel特征。
相比之前的算法,MOSSE能够形成更加明确的峰值,减少了漂移;此外,MOSSE可以在线更新,同时还采用了PSR来检测遮挡或者跟丢的情况,从而决定是否需要停止更新。
值得一提的是,MOSSE在做相关操作之前,对每张图都进行了减去平均值的处理,这有利于淡化背景对相关操作的影响。另外假如发生光照变化的话,减去均值也有利于减小这种变化的影响。此外要注意,输出的特征应乘以汉宁窗(一种余弦窗),用于确定搜索区域(也就是不为0的区域),且有利于突出中心的特征。

TLD

TLD(Tracking Learning Detection)主要针对long-term tracking,在跟踪的同时全局检测。它由三部分组成:跟踪模块、检测模块、学习模块。
跟踪模块观察帧与帧之间的目标的动向。作者采用了光流来跟踪,此外还提出了一种判断跟踪失效的算法,由于光流跟踪时选取的若干特征点,当其中某一个特征点的位移与所有特征点位移的中值之差过大时,也就是某个特征点离跟踪模块认为的目标中心位置很远时,就认为跟踪失效。作者还通过相似度和错误匹配度来对特征点进行筛选。
检测模块把每张图看成独立的,然后对单张图片进行目标检测定位。作者使用了方差检测器、随机森林和最近邻分类器来对目标做检测。
学习模块对根据跟踪模块的结果对检测模块的错误进行评估,当置信度较低时,重新组织正负样本对随机深林的后验概率和最近邻分类器的在线模板进行更新,从而避免以后出现类似错误。
TLD与传统跟踪算法的显著区别在于将传统的跟踪算法和传统的检测算法相结合来解决被跟踪目标在被跟踪过程中发生的形变、部分遮挡等问题。同时,通过一种改进的在线学习机制不断更新跟踪模块的“显著特征点”和检测模块的目标模型及相关参数,从而使得跟踪能够自适应,效果较之前更加稳定、可靠。

2011

FoT

FoT(Flock of Trackers)首先在目标上抓取多个interesting point并分别放入多个cell中,之后的跟踪就是检测并补偿每个cell中interesting point的偏动量,使其回到中间。如果interesting point超出cell,则让它重新恢复到cell的中点。

此外,FoT还提出了两种简单有效的failure预测方法:neighbourhood consistency predictor(Nh)和Markov predictor(Mp)。Nh的基本思想是认为正确的跟踪情况下每个local tracker给出的位移应当与它相邻的tracker相一致,而Mp主要是针对时域一致性,认为前几帧表现较好的local tracker在当前帧也会有较好的表现。FoT基于这些failure预测方法来控制模型的更新。

Struck

Struck的主要贡献是引入了结构化SVM。考虑到传统的跟踪算法将跟踪问题转化为一个分类问题,并通过在线学习技术更新目标模型。然而,为了达到更新的目的,通常需要将一些预估计的目标位置作为已知类别的训练样本,这些分类样本并不一定与实际目标一致,因此难以实现最佳的分类效果。
结合上述考虑,Struck利用了结构化SVM直接输出跟踪结果,避免了中间分类环节,这使得在当时效果有明显的提升。同时,为了保证实时性,Struck还引入了阈值机制,防止跟踪过程中支持向量的过增长。

L1 Tracker

L1 Tracker是第一个将稀疏编码引入目标跟踪问题中的算法。它把跟踪看做一个稀疏近似问题,主要是用第一帧和最近几帧得到的图像(特征)作为字典,通过求解L1范数最小化问题,实现对目标的跟踪。

MIL

MIL采用了多示例学习的方法而不是传统的监督学习(即由原本单独标记的示例变成一组示例,当且仅当所有的示例都判定为负才认为是负,只要有一个示例判定为正则整组都判定为正),对于不精准的tracker和错误标注的训练样本有鲁棒性的提升。

2012

CSK

CSK也称为核相关滤波算法,作者针对MOSSE做出了一些改进,作者认为循环移位能模拟当前正样本的所有的转换版本(除边界以外),因此采用循环移位进行密集采样,并通过核函数将低维线性空间映射到高维空间,提高了相关滤波器的鲁棒性。这里循环移位后的样本匹配可以理解为如果某个候选区域与某一个移位样本的相关操作响应较高,那么就可以理解为物体的移动和该样本移位的方式一致,从而对下一帧目标位置进行定位。
随后的工作主要从特征选择、尺度估计、正则化等方面对该算法进行改进和提高。关于循环移位和线性、非线性的核函数计算,我在之前的文章中做了一些分析,感兴趣的话可以看看。

DF

DF发现之前在图像中寻找目标的梯度下降方法首先会模糊图像来平滑目标方程,这就会严重损害目标的位置信息。因此作者提出了对每一个像素点设置多个通道,在每个通道进行卷积,这种方法同样也能平滑目标方程但不会严重损害目标的位置信息。其实这就是之后的CNN能做到的,在当时应该也算是一种创新。

CT

CT(Compressive Tracking)是一种基于压缩感知的高效跟踪算法。和一般的判别式模型架构一样,CT首先利用符合压缩感知RIP条件的随机感知矩阵对图像特征进行降维,使得到的低维信号可以完全保持高维信号的特性并可以完全重建,然后在降维后的特征上,在感知空间下采用朴素贝叶斯分类器进行分类。另外,CT在每一帧通过在线学习更新分类器,在线学习的样本来自通过相同的稀疏感知矩阵提取的前景目标和背景的特征。

ORIA

ORIA假设前一帧是完美的,于是把跟踪问题视作将下一帧图像与上一帧进行对齐,也就是一串连续的凸优化问题。

2013

下面是VOT2013的排名结果,其中Experiment 1是在所有序列上使用ground truth初始化的实验结果,Experiment 2使用含噪声(10%的尺寸扰动)的ground truth,Experiment 3使用灰度图像。

PLT

PLT通过一个固定大小的、基于二值特征向量的线性分类器对每一张图像做分类,得分最高即为目标。作者利用一个稀疏的在线结构化SVM来选出一个小的判别特征集合。在训练SVM时,考虑到在bounding box内的像素不一定都属于物体,作者使用了一种基于概率的掩模来分配权重,然后计算初始的结构化SVM,去除分值最小的特征。由于特征向量的二值性,该线性分类器可以作为查找表用于快速检测。

EDF

EDF是DF的加强版,在DF的基础上探索了每一个通道之间的联系。

LGT

LGT针对模型何时更新与更新哪些部分的问题,考虑到目标模型的整体更新会损失部分有用信息和固定的分块不利于应对目标的变化,借鉴了之前将目标有结构地分块且动态删减的思想,提出了一种由patch组成的集合构成的、用于精确定位的local layer和颜色、移位、形状三个特性组成的、用于指导增加patch的global layer。

对于新输入的一帧图像,LGT的处理流程如下:

  1. 首先使用卡尔曼滤波器结合近似匀速模型来确定目标的位置。接下来几步是对位置进行微调。
  2. 对于每一个patch,作者用一个统一的仿射变换和一个独立的微小扰动在5维空间(位置2维+尺度2维+旋转1维)定义其对于初始patch的变换。这里的$\widehat{x}$指的是初始patch。作者把$A_{t}^{G}$和$\Delta _{t}$中的参数看作正态分布的。先使用交叉熵方法反复迭代寻找最优解,当协方差矩阵的行列式小于0.1(各分量相关性很强)时停止迭代,随后把仿射变换矩阵的参数(也就是学到的正态分布的均值和方差)固定并用于所有的patch。接下来对每一个patch,用同样的交叉熵方法迭代,得到每一个patch的微小扰动的均值和方差。
  3. 结合visual consistency和drift from majority两种估计,更新每一个patch在当前帧的权重,并与前一帧的权重加权求和来确定最终的权重。这里的权重决定了每一个patch在最终所有patch混合决策时的重要性。
  4. 利用上面的patch重新计算之前卡尔曼滤波器的结果,确定目标的位置。
  5. 接着进行local layer中patch的删减与增补。对于权重小于阈值的patch作删去处理;对距离很近的两个patch进行合并,合并后产生的patch的所有参数设为合并前两个patch参数的平均。使用剩下的patch更新global layer,然后用更新后的global layer决定是否以及如何增加新的patch。为了防止突然过度增加patch,作者对增加的样本数施加上限限制,并利用加权的方式平滑调整样本容量。
  6. 进入下一帧。

LGT++

LGT++是LGT的改进版。在LGT的基础上,LGT++增加了memory、failure detection和用粒子滤波代替卡尔曼滤波的recovery机制。此外对尺度变化和背景干扰也做出了改进。

DLT

DLT是最早的基于深度学习的算法(当时AlexNet刚刚被提出),它采用了堆叠去噪自编码器网络,把跟踪视为一个分类问题,直接利用80 Million Tiny Images数据集上的预训练模型提取深度特征,这种强行task转换的训练方法存在缺陷,但在当时是个进步。

STC

STC(Spatio-Temporal Context)通过贝叶斯框架目标时间、空间的上下文信息来建模,利用得到的关系结合生物视觉系统中的注意力特性来生成confidence map来预测目标位置。由于上下文信息建模和之后的预测都采用了快速傅里叶变换,因此算法的速度很快。

文章主要举了两个例子来说明空间信息的重要性。当目标物体被部分或者完全遮挡时,周围的信息能帮助定位被遮挡的目标(假设摄像头不移动),也就是说可以利用空间的距离信息;此外,如果目标内部的两个部分比较相似(比如人的一对眼睛),就比较容易发生偏移,而如果这时恰好这两个部分的距离信息相似(距离目标中心长度相同),那就需要引入相对位置也就是方向信息来判断。
此外,考虑到生物视觉系统中的注意力特性,作者增加了一项权重函数来构成先验,该函数根据距离目标位置的远近来定义。
作者还对confidence map的参数进行了讨论,认为置信度在空间上的分布不能太平滑(增加位置模糊不确定性),也不能太尖锐(导致过拟合)。
作者认为目标的形态与近几帧有较强的关联,由此设计了时域信息模型。文中提到的时域滤波器可被证明是低通的,也就是可以滤去一定的噪声。此外,STC还设计了尺度更新方法,最终下一帧的尺度是前n帧估计尺度的均值。

LT-FLO

LT-FLO主要针对的是缺少纹理特征的目标,使用边界点来代替在目标上采集点来做跟踪。此外作者还提出了一种基于边界梯度稳定性的failure检测机制。

GSDT

GSDT提出了一种采集正负样本进行图嵌入的判别式模型。作者使用了基于图结构的分类器而不是生成一个子空间。此外GSDT还设计了一种新的图结构来区分类内的不同和样本的内在结构。

SCTT

SCTT使用了treelets降维方法,由于仅选取较高置信度的样本,相对于PCA只需要更少的样本且对噪声有更好的鲁棒性。

CCMS

CCMS(Color Correspondences Mean-Shift)用之前提到的Mean Shift方法对目标候选与目标模型、目标候选与背景模型在每种颜色(也就是直方图中每个bin)中计算相似性,反复迭代,直到收敛或者达到最大迭代次数为止,如此来进行运动估计。

Matrioska

Matrioska基于特征点提取的方法(ORB、FREAK、BRISK、SURF等),考虑到目标物体的外观变化和有利于增强模型表现力的负样本提取,使用了增枝和剪枝的方式来对目标模型进行更新。

AIF

AIF(adaptive integrated feature)提出了一种评估特征稳定性的方法,并根据不同特征的稳定性动态地分配权重。

HT

HT借鉴霍夫森林,也就是霍夫变换结合随机森林,相比一般的随机森林增加了位移信息。HT将目标分割成多个图像块,这些图像块含有它们各自偏离目标中心的向量$d$,对每个图像块提取特征描述子,这样就构成了正样本,即$y=1$;在图像的其他区域也提取同样尺寸的图像块,也提取特征描述子构成负样本,即$y=0$,注意负样本的偏移向量$d=0$。由此训练生成树,再由树构成森林。
在跟踪时,将每个图像块输入训练好的森林里,最终会落到森林的每棵树的一个叶节点上,这就得到了该图像块相对目标中心的偏移向量$d$以及概率$p$,随后将每棵树上的结果加权平均,得到了该图像块的结果。最后将所有的目标分割出的图像块的结果组合起来,得到目标预测结果。
HT的一个好处就是可以调整bounding box的长宽比,此外作者认为这对非刚性或者铰接的目标也有很大好处。

STMT

STMT把目标跟踪分成镜头运动估计和目标运动估计两个阶段,先估计摄像头的运动并进行对齐,然后再定位下一帧的目标位置。

ASAM

ASAM(Adaptive Sparse Appearance Model)用一个样本集来表示目标的各种变化,并且基于判别式和生成式的稀疏表示,使用了第一帧、后续帧两阶段的在线跟踪算法。

2014

下面是VOT2014的排名结果,这里的A表示accuracy,R表示robustness。

DSST

DSST主要考虑了尺度缩放的问题。它将目标跟踪看成位置变化和尺度变化两个独立问题,提出了一个高、宽、尺度数三维的滤波器,使用先计算平移位置再聚集尺度的“两步”法,即训练了两个滤波器,首先训练位置平移相关滤波器以检测目标中心平移,然后训练尺度相关滤波器来检测目标的尺度变化。

CN

CN(Color Naming)考虑到在遇到光照变化、形变、部分遮挡、背景干扰等问题时,颜色特征相比灰度特征能提供更丰富的信息以取得更好的效果,引入了颜色特征来扩展CSK,它将目标RGB(红绿蓝)三维空间的颜色特征映射为黑、蓝、棕、灰、绿、橙、粉、紫、红、白和黄11维空间的颜色特征的多通道颜色特征,后又降维至2维以保证实时性。Color Naming较RGB三原色特征更符合人类的感觉,对目标的表征能力更强,而且具有一定的光学不变性。

SAMF

SAMF也考虑了尺度问题,思路比较简单,采用k个尺度去采样,由于核相关操作的点乘需要固定尺度的输入,因此对采集到的样本作双线性插值成为固定尺度,然后再做相关操作。在特征方面,SAMF发现HOG和Color Naming有互补作用,考虑到和相关操作仅包含点乘和向量范数的计算使得多通道很容易被引入,因此使用了HOG和Color Naming多通道特征。

KCF

KCF跟CSK是同一个团队提出的,它跟CSK的区别是就是作者对循环性质进行了完整的理论推导,引入HOG特征并提供了一种把多通道特征融合进相关滤波框架的方法,对CSK作了进一步的完善,是一个具有里程碑意义的工作。算法的详解和一些数学理论可以看看我之前的文章。

DCF

DCF与KCF出自同一篇paper,不同的是KCF使用的是高斯核,DCF使用的是线性核。

注意:这里的DCF(Dual Correlation Filter)和之后一些文章中提到的DCF(Discriminative Correlation Filter)是两个不同的概念,请注意,别搞错了。

FCT

FCT(Fast Compressive Tracking)和CT一样,也是使用了压缩感知,主打速度。相比之前,FCT提出了一种由粗到精的搜索策略,而不是穷尽搜索。首先在一个较大的搜索半径内选择一个较大的搜索步长,得到一个粗糙的位置,然后以该位置为中心,在一个较小的搜索范围内,以一个较小的搜索步长进行搜索,最后得到跟踪目标的位置,这样就能在不降低最终精度的前提下加速寻找过程。由于可证明CT特征具有尺度不变特性,FCT在采集候选区域时增加了尺度因子,即在同一位置采集三个尺度的候选区域,从而得到当前帧的尺度。此外,作者采用了每5帧更新一次尺度的策略。

CMT

CMT用成对的特征点之间角度的变化来判断目标的旋转情况,此外还用特征点投票的方式来确定目标的位置。为了避免尺度变化引起的投票不准,也就是从特征点出发指向目标位置的投票向量越过了目标中心或者没达到目标中心,作者用欧氏空间和原图像空间之间各对特征点之间距离的比值来进行修正。基于投票出的目标位置的聚类,作者还给出了一种一致性的判别方法,将聚集最多数特征点的投票位置视为一致性聚类,并将投票至其他位置的特征点视为错误从而移除。

2015

下面是VOT2015的排名结果。

SO-DLT

SO-DLT针对DLT的缺陷进行改进,使得CNN更加适用于目标跟踪。由于目标跟踪的目的是将物体从背景中分离出来而不是全图识别,DLT的训练方法和标签化输出就不是很合适了。
但是,由于当时跟踪方向标注数据的匮乏,作者还是不得不使用ImageNet图像检测数据集来进行预训练,事实证明这是有效的,因为目标检测和目标跟踪两个不同的task中存在一样的共性信息。不同于标签化的单个数值输出,SO-DLT输出的是一个50x50的像素级的概率图。
由于上述训练方法训练的是CNN从非物体中提取出物体的能力,因此在实际跟踪接收到第一帧时,还需根据目标对网络进行微调,否则会跟踪出视频或者图像序列中所有的无论是目标与否的物体。
类似DSST,SO-DLT采用的是先预测目标中心位置,然后再从小到大确定尺度的策略,如若扩展到预设的最大尺度来检测的概率图依旧达不到要求,则认为已经跟丢目标。
此外,为了提升鲁棒性,作者采用了两个CNN网络共同决策而以不同方式更新的策略。两个网络分别针对的是short-term和long-term。针对short-term的网络在负样本的概率图响应和超过一定阈值时进行更新,为的是防止负样本与目标响应近似而导致漂移;针对long-term的网络在当前帧预测结果的置信度达到一定水平以上时才进行更新,因为此时可认为框出的目标较为可信。
每次更新时需要采集正负样本,SO-DLT对正负样本的提取方法比较简单,在目标位置及周围形成一个类似九宫格的区域,在中间格用四种尺度提取正样本,对周围的8格采集负样本。

MDNet

MDNet设计了一个轻量级的小型网络学习卷积特征表示目标。作者提出了一个多域的网络框架,将一个视频序列视为一个域,其中共享的部分用来学习目标的特征表达,独立的全连接层则用于学习针对特定视频序列的softmax分类。
在离线训练时,针对每个视频序列构建一个新的检测分支进行训练,而特征提取网络是共享的。这样特征提取网络可以学习到通用性更强的与域无关的特征。
在跟踪时,保留并固定特征提取网络,针对跟踪序列构建一个新的分支检测部分,用第1帧样本在线训练检测部分之后再利用跟踪结果生成正负样本来微调检测分支。
此外,MDNet在训练时还采用了难例挖掘技术,随着训练的进行增大样本的分类难度。

SRDCF

SRDCF主要考虑到若仅使用单纯的相关滤波,可能会存在边界效应,也就是相关滤波采用循环移位采样导致当目标移位到边缘时会被分割开,此时得到的样本中就没有完整的目标图像从而失去效果。
于是,作者采用了大的采样区域,用两倍区域进行循环移位,这就保证了无论如何移位,获得的样本中都能有一个完整的目标存在。
然而,由于上述移位方式会导致背景信息被夹在横向以及纵向的两个样本之间而出现在图片的中间区域,这会导致模型判别力的退化。因此作者在移位之前先在滤波器系数上加入权重约束(类似于惩罚项):越靠近边缘权重越大,越靠近中心权重越小。这就使得滤波器系数主要集中在中心区域,从而让背景信息的影响没有那么明显。尽管作者采用了埃尔米特矩阵的共轭对称性来提升计算量,但是由于SRDCF破坏了原本闭式解的结构使得优化问题只能通过迭代求解,因此速度比较缓慢。

DeepSRDCF

DeepSRDCF在SRDCF的基础上,将handcrafted的特征换为CNN的特征,关注点也在解决边界效应。作者使用DCF作为网络的最后一层,也就是对之前卷积网络输出的每一个通道的CNN特征都训练一个滤波器用于分类。作者还对不同的特征进行了实验,说明了CNN特征在解决跟踪的问题采取底层的特征效果会比较好(DeepSRDCF仅用了PCA降维处理的第一层),说明了跟踪问题并不需要太高的语义信息。

HCF

HCF的主要贡献是把相关滤波中的HOG特征换成了深度特征,它使用的是VGG的3、4、5三个层来提取特征,针对每层CNN训练一个过滤器,并且按照从深到浅的顺序使用相关滤波,然后利用深层得到的结果来引导浅层从而减少搜索空间。

FCNT

FCNT较早地利用CNN网络底层和顶层不同的表达效果来做跟踪。不同于以往的工作把CNN看成一个黑盒而不关注不同层的表现,FCNT关注了不同层的功能,即发现:顶层的CNN layer编码了更多的关于语义特征的信息并且可以作为类别检测器;而底层的CNN layer关注了更多局部特征,这有助于将目标从目标中分离出来。这个发现在之后的许多工作中也得到了应用和体现。如下图所示,这里的a图表示的是ground truth,b图表示的是使用VGG的conv4-3,也就是第10层产生的热力图,c图是通过conv5-3也就是第13层产生的热力图。

可以看到,较低维的CNN layer(conv4-3)能够更精准地表示目标的细粒度信息,而较高维的CNN layer(conv5-3)热力图显示较模糊,但对同类别的人也做出了响应。这就是说,顶层缺少类内特征区分,对类间识别比较好,更适合作语义分割;底层则反之,能够更好地表达目标的类内特征和位置信息。
基于不同层(顶层和底层)之间提取特征的不同,作者提出了一种新的tracking方法,利用两种特征相互补充辅助,来处理剧烈的外观变化(顶层特征发挥的作用)和区分目标本身(底层特征发挥的作用)。由于feature map本身是有内在结构的,有很多的feature map对目标的表达其实并没有起到作用,因此作者设计了一种方法来自动选择高维CNN(GNet)或者低维CNN(SNet)上的feature map,同时忽略另一个feature map和噪声。在线跟踪时,两个网络一起跟踪,采用不同的更新策略,并在不同的情况下选择不同的网络输出来进行预测。
顺便提一下,为了简化学习任务,降低模型复杂度,作者采用了稀疏表示的方法。
关于FCNT的一些相关概念和具体按步骤的细节实现,可以参考一下我之前写的文章。

LCT

LCT主要针对的是long-term tracking的问题。作者配置了一个detector,用于跟丢之后快速重检测。LCT用了两个滤波器,一个是用于平移估算的$R_{c}$,使用padding并施加汉宁窗(一种余弦窗),结合了FHOG和一些其他的特征;另一个是用于尺度估计的$R_{t}$,不使用padding和汉宁窗,使用HOG特征,此外$R_{t}$还用于检测置信度,用来决定是否更新模型和是否重检测。

CCT

CCT借鉴KCF中kernel trick的特性和DSST中将定位和尺度估计两步分离的思想,对DSST只利用本来的特征空间表征目标的不足进行改进,在核特征空间对目标进行表征并用尺度因子扩展KCF的核相关滤波器,也就是为了保证计算效率和连贯性,利用尺度因子将每一帧目标尺度都统一为初始帧的目标尺度,然后使用核相关滤波器进行位置估计。然后,通过和DSST一样的方式再进行尺度估计。
为了应对漂移的问题,CCT使用了一个在线CUR滤波器。CUR矩阵分解可以近似的表示原矩阵A,其中C是A的列而R是A的行,两者通过一种固定的方式从A中随机采样形成,这既保证了A的内在结构,可以反映A的低秩属性,也可以看作一种映射,即将过去的目标表征矩阵投影到一个可被证明具有误差上界的子空间。此外,作者还引入了一个基于失败检测的自适应学习率调整方法。

CFLB

CFLB讨论了循环移位带来的边界效应的问题,提出了在目标外围扩大尺寸来进行循环移位的方法,使得有效样本的比例大大提高。具体来说,就是给原来的循环移位样本左乘一个列数远大于行数的掩模矩阵。此外,由于扩大尺寸后部分参数要在空间域而不是频域计算会导致效率降低,作者利用了增广拉格朗日方法(即在拉格朗日方法的基础上添加了二次惩罚项,从而使得转换后的问题能够更容易求解,不至于因为条件数变大不好求)来解决这个问题。

KCFDP

KCFDP借鉴了目标检测中detection proposal的思想(主要用于减少计算和提高质量),来解决之前DCF系列算法中的尺度和长宽比变化的问题。对每一帧,KCFDP首先用KCF对上一帧输入(准确的说是之前每一帧的加权累积)作操作,得到当前帧的位置和响应分数$v$;随后利用EdgeBoxes(一种detection proposal方法)在KCF预测的位置周围搜寻proposal,选取其中的前200个,并排除其中与之前KCF得出的预测目标IoU大于0.9(认为结果一样,无需考虑)或者小于0.6(认为误判,不是目标);最后,在剩余的proposal中,选择得分最高的proposal,与之前的响应分数$v$作比较:若小于$v$,则把KCF的结果作为预测结果且不更新尺度和长宽比(KCF算法本身具有该功能);若大于$v$,则把该proposal作为预测结果,并利用该proposal的尺度和长宽比来更新目标的参数。

HCFT

HCFT构造了一种阶梯式的深层至浅层由粗到细的定位方法,结合深层网络的语义信息和浅层网络的高分辨率位置信息,其网络结构如下(和FPN很像)。

为了保持分辨率相同,作者对池化后的深层输出再进行双线性插值以复原原来的分辨率。假设$l$层最大响应处的坐标为$(\widehat{m},\widehat{n})$,HCFT通过以下式子来确定$l-1$层目标的位置。

第二行的约束是为了浅层细粒度的位置需保持在深层粗粒度的位置附近,这样便完成了由粗到细的定位方法。
此外,在训练过程中需要采集正负样本,由于正负样本边界难以区分的模糊性和二值(也就是0,1标注)的正负样本的绝对性导致一点微小的正负样本区别就会导致drift。为此,作者将训练样本的标注回归到高斯方程的平滑标签。

MUSTer

MUSTer模拟了人脑的记忆过程,类似于LSTM那样分成short-term和long-term两种memory,使用了相关滤波(short-term)和特征点检测(short-term+long-term),最后根据两种记忆形式的提供的输出来决策和进行滤波器的更新。
人脑的记忆分为感官、短时记忆、长时记忆三个阶段,MUSTer的设计基本采用了这样的三个step,如下图所示。

MUSTer的结构比较“纵横交错”,下面我对一些比较重要的部分做一个概述。
短时记忆和长时记忆都由特征点的集合构成。特征点数据集包括目标和背景两种样本,其中背景主要是用于遮挡的判断,即当位于bounding box中的背景特征点与目标特征点的比例超过一定值时,认为此时发生了遮挡。
对于特征点的匹配,作者采用了最近邻方法,在欧几里得空间根据余弦相似度(也就是两个向量余弦夹角的大小,角度越小,余弦值越大,相似度越高)来计算匹配置信度。为了判别离群值(outlier),还需计算第二近邻的相似度,如果第一近邻的相似度比上第二近邻的比值小于某个阈值,就说明该处特征点比较集中应该不是离群值。
长时记忆模块通过RANSAC估计的一个版本——MLESAC(引入似然度)来决定目标的状态,从而与相关滤波的输出结合。
短时记忆在每一帧都进行更新,若根据前面所说的方法判定为遮挡,则清空短时记忆;若此时并没有判定为遮挡,则用RANSAC估计输出的内围值(inlier)来替换之前的短时记忆。此外,为了避免多余的特征点出现,作者用网格划分目标template并根据相对位置分配ID,若出现重复的ID,则认为两者中之前的特征点是多余的。
长时记忆只在判断跟踪成功和无遮挡时进行更新。作者认为匹配失败的特征点能够表示目标发生变化的重要信息,因此长时记忆更新是针对匹配失败的点进行的,将匹配失败且位于bounding box外面的点移入背景数据集,而将匹配失败且位于bounding box内部的点移入目标数据集。模拟人脑,长时记忆采用的是一种对数形式下降的遗忘曲线。

RPAC

RPAC将相关滤波器应用于分块跟踪,并且借鉴了粒子滤波中的贝叶斯估计的思想,在提升鲁棒性的同时保证了速度。
作者对每一区块(part)都使用一个独立的相关滤波器,使用了PSR(体现置信度)和时域顺滑程度(用于判断遮挡等情况)两者结合来分配每一区块的权重,同时仅当这个权重大于阈值时才更新对应的滤波器以达到自适应更新的效果。这里每一个区块的输出都是一个confidence map,最后需要根据权重和相对位置组成一个大的confidence map用于接下来的预测。
为了防止部分区块漂移的问题出现,作者采用了贝叶斯估计框架,即选择使得状态(一组仿射运动参数)先验值最大的候选区域作为结果,考虑到多个confidence map之间重叠的部分直接求和会出现叠加的较大值而影响概率估计,作者根据每一个区块的最大响应值和尺寸来施加余弦窗,从而抑制多张图中较小值的叠加改变某些区块位置上的响应分布。
此外,对于偏离较远的区块,RPAC采用自动丢弃并利用其他区块重新生成的方法。得益于分块的方式,使得tracker对尺度变化也有适应性。

RPT

RPT把目标物体看作一系列具有相似运动轨迹的patch的集合。作者基于粒子滤波的框架,用patch的两个属性来定义每个patch的可靠性:可追踪性和目标附着性。可追踪性直接用KCF输出的响应图取PSR来定义;目标附着性根据每个patch在近k帧的运动轨迹来定义,具体来说,就是认为正样本与其他正样本的运动轨迹是一致的且远离负样本,同时负样本与正样本的运动轨迹有很大的差别。其公式定义如下,其中$y_{t}$是正样本或者负样本的$\pm 1$标签(正负样本用bounding box来分割)。

由于粒子滤波是用函数(这里是可靠性函数)对后验概率分布做近似,由于无法达到理想状态(即完全一致),因此为了避免噪声的累积,粒子滤波类算法需要重采样来不断补入正确的信息。RPT采用的是不断补入新样本的方法而不是全部重采样替换的方法,作者在两种情形下采样新的样本:当正样本或者负样本其中一者的比例过高时,采集新样本来平衡;当跟踪置信度(这里定义为PSR)较低时,采集新样本,这种情况往往出现在遇到缺乏纹理的目标物体时。此外,作者对离目标过远的patch进行舍弃,具体就是划定一个比bounding box更大的矩形框,对超出这个矩形框的patch做舍弃。
可以说,作者把patch分成三类。第一类是positive patch,也就是在目标上的;第二类是贴着目标周围一圈的negative patch,这对下一帧区分物体和背景很有帮助;第三类就是离目标很远的negative patch,这些patch的作用就比较弱,因此舍弃。

2016

下面是VOT2016的排名结果。

DLSSVM

DLSSVM延续之前的Struck,利用结构化SVM,在优化的阶段做了一些改进进行提速。其实结构化SVM分类器非常强大,但是因为它求解优化的过程比较复杂以及使用稠密采样(粒子滤波或者滑窗采样)比较耗时,使得结构化SVM的速度成为一个瓶颈,因此不如一些使用相关滤波的SOTA的算法。

C-COT

C-COT(连续空间域卷积操作)发现单一分辨率的输出结果存在扰动,因此作者想到利用CNN中的浅层表观信息和深层语义信息相结合。然而之前的DCF系列算法仅能使用单一分辨率的特征图,这就无法使用预训练CNN中不同分辨率的不同层,这是限制其效果的重要因素。因此,作者提出一种连续周期的插值运算符以利用不同空间分辨率的响应,在频域进行插值得到连续空间分辨率的响应图,最后通过迭代求解最佳位置和尺度(用0.96,0.98,1.00,1.02,1.04五种缩放倍率去搜索)。
和名称一样,C-COT最重要的贡献是它把图像$N_{d}$个像素点的离散分布变成了周期为$T$的连续空间响应图。由于本文的理论功底很深,我之前并没有看懂,后来经学长的指点才有所感悟。事实上,这里的插值运算符做的并不是插值的事,而是用一种近似的方式将离散信号重构为连续信号。虽然会导致计算量剧增,但这是一个很重大的突破。
文中还提出了一种使得各个分辨率通道的特征自然融合至相同分辨率的方法,这里相同分辨率可理解为最后的各个响应图在空间上拥有相同的样本点数。作者首先对各个不同分辨率的通道进行插值,然后使用对应的滤波器在连续的空间域内卷积,最后将响应求和得到最终的置信度方程。
C-COT使用的是类似SRDCF的框架,也引入了空间正则项,当远离目标中心是施加较大的惩罚,这使得能够通过控制滤波器的大小来学习任意大小的图像区域。C-COT在每一帧也会采集一个训练样本,根据过去帧数的远近来设置每个采集样本的重要性权重(每次都做归一化),并且设置了最大的样本容量,当超出容量时删去重要性权值最小的样本。不同于SRDCF使用Gauss-Seidel迭代法,C-COT使用Conjugate Gradient方法来提高效率。
得益于浅层特征的高分辨率,C-COT能够达到sub-pixel的精度,也就是仅次于像素级别的精确度。位置细化的过程就是上面所说的用共轭梯度法迭代的过程,在C-COT的代码中有一个迭代次数设置,被设置为1,即就使用一步迭代优化后的位置。换句话说,在当前长时跟踪算法本身误差之下,更精细的位置意义不大。

SRDCFdecon

SRDCFdecon针对在线跟踪时采集的样本中有一部分质量不佳的问题,不同于之前把采样和样本的选择作为一个独立的模块,作者提出了一种将样本权重统一到模型参数中的损失函数。
不同于之前“加入训练集or舍弃”这样二选一的样本选取方式,SRDCFdecon使得样本的重要性权重连续,同时在跟踪的过程中能够完成权重的重新分配和先验的动态变化。这里的先验其实可以看作对权重的一种约束,使权重在近几帧逐渐增大。作者用一种比较巧妙的方式来控制先验的影响力,具体来说,对权重的正则项为$\frac{1}{\mu }\sum_{k=1}^{t}\frac{\alpha _{k}^{2}}{\rho _{k}}$,当$\mu$趋向于无穷时,损失函数求解得到仅有当前帧的权重$\alpha$趋向于1,相当于丢弃了之前的样本,仅接收并保存当前帧的新样本;当$\mu$趋向于零时,$\alpha$将趋向于先验$\rho$。

Staple

Staple提出了一种互补的方式。考虑到HOG特征对形变和运动模糊比较敏感,但是对颜色变化能够达到很好的跟踪效果,color特征对颜色比较敏感,但是对形变和运动模糊能够有很好的跟踪效果,因此作者认为若能将两者互补就能够解决跟踪过程当中遇到的一些主要问题。于是,Staple使用HOG-KCF与color-KCF结合算法对目标进行跟踪,速度很快,效果也很好。

SINT

SINT运用匹配学习的思想,最早地把孪生网络(Siamese Network)应用于目标跟踪。它通过孪生网络直接学习目标模板和候选目标的匹配函数,并且在online tracking的过程中只用初始帧的目标作为模板来实现跟踪。

TCNN

TCNN使用一个树形的结构来处理CNN特征。作者利用可靠性来分配预测目标的权重,采用的更新策略是每10帧删除最前的节点,同时创建一个新的CNN节点,选择能够使新节点的可靠性最高的节点作为其父节点。这样一直保持一个active set,里面是10个最新更新的CNN模型,用这个active set来做跟踪。TCNN效果较之前有一定提升,但是速度比较慢,而且比较消耗存储空间。

SKCF

SKCF把之前KCF中用于确定搜索区域的余弦窗换成了高斯窗,这么做有两点主要的好处。
首先,当搜索区域固定时,余弦窗的带宽就是固定的了,而高斯窗则可以通过调整方差来改变中间响应比较高的区域的宽度。可以这么理解,我们从二维的余弦函数和高斯函数来看,假设搜索区域的宽度是$\pi $,那么距离边缘$\frac{\pi }{6}$的位置一定是中间最大值下降一半的位置,而高斯函数的形状则还受方差控制。这一特性使得高斯窗在搜索区域确定时能够更好地适应目标尺寸,此外论文中还提到了这种方法能方便减轻计算量。
此外,高斯分布的傅里叶变换依旧是高斯分布。这种特性可以抑制频谱泄露的问题。简单来说,频谱泄露就是信号从时域转化到频域后,除了原本应该出现的谱线,其旁边还会漏出一些小的频谱,从而造成干扰。因此,抑制频谱泄露能够保持前景与背景在频域内的区分度。
SKCF还改进了之前一些基于特征点的算法的不足。由于之前的一些算法在最终决策时用矩形框来划定有效的特征点,并分配相同的权重做出决策。作者认为这样相当于间接的认为目标是矩形的,而大多数目标的几何结构往往不是矩形。于是SKCF对各个特征点采用从中心到周围逐渐减少权重分配的方式,能更好地适应目标的几何结构。
SKCF还是用了英特尔CCS(复数共轭对称)文件格式,无论是用在SKCF还是KCF上,计算速度相比原来的KCF都提升了将近一倍,或许是共轭对称减少了一半计算量。

MRF

MRF提出了一种基于马尔科夫随机场的模型,挖掘各个patch和目标之间的联系(弹性能量)并判断每个patch的遮挡情况。作者发现当响应值较低时并不能认为该patch被遮挡了,也有可能是目标外观变化等情况。此外作者还发现当发生遮挡时,会出现较大的响应值分散分布的现象。于是当patch中响应值高于$\eta s_{max}^{k}$的像素个数占比高于阈值时(这里的$\eta$是一个小于1的因子),就认为该patch被遮挡。
对于尺度变化,作者发现当尺度变小时patch之间的重叠会变多,因此通过计算初始帧的patch距离和当前帧的patch距离之比来确定尺度缩放的比例。
不同于之前的马尔科夫随机场模型,作者在文中还给出了一种高效的置信度传播方法。

GOTURN

GOTURN提出了一种类似孪生网络的框架,将裁剪过的前一帧和当前帧分别通过在ImageNet上预训练的backbone提取特征,然后用三层全连接层进行特征比较并进行回归。其中前一帧的图像在裁剪时将其置于裁剪后图像的中心,并在周围做一定的扩充以吸收更多上下文信息。当前帧裁剪区域也就是搜索区域由上一帧的位置来确定。
GOTURN最突出的贡献就是把基于深度神经网络的速度第一次达到了100FPS。作者通过在视频和静态图像上进行离线训练,在跟踪时不更新来达到这种效果。由于训练集的缺乏,作者冻结之前在ImageNet上预训练的CNN权重,在离线训练时仅更新FC层。考虑到实际跟踪时的特点,作者还设计一种平滑的运动模型。作者发现目标相邻两帧中心点的坐标关于尺度的增量呈均值为0的拉普拉斯分布(类似高斯分布,只不过中间是尖的),也就是说一般物体逐帧的运动是较小的。因此,作者对训练数据做增广处理,使得随机裁剪得到的样本能服从拉普拉斯分布。由于训练方式和网络设计,使得GOTURN仅对目标敏感而不是对类敏感(比如行人检测能检测各种行人而不能检测车)。

SiamFC

SiamFC使用孪生网络来解决数据稀少和实时性要求对深度学习在目标跟踪中的限制。作者以第一帧的BBox的中心为中心裁剪出一块图像,并将其缩放至127x127作为template,并保持不变。在后续帧中,search image也用类似的方法得到。分别将template和search image通过5层不带padding且不在线更新的AlexNet,然后用互相关层做相关操作得到输出的score map。响应最大值和中心的偏差表示位移。此外作者用一个小批量的不同尺度的图像去前向传播来实现多尺度判断。
由于对于search image来说,表示通过CNN的卷积方程是全卷积的,因此可以使用比template大的search image,也就是可以在它上面的各个子窗口进行计算。
基于卷积的平移等变性,我们可以通过score map得到目标的位置,即当目标平移了$n$时,相应的就会在score map上平移$\frac{n}{stride}$。之所以仅使用5层而不是更深的网络,是因为SiamFC没有使用padding来使得网络能够更深。之所以不使用padding,是因为一旦加入了padding,会使得图像边缘像素的响应值在平移的同时会发生改变,这就不利于最后的定位了。具体来讲,就是当目标处于画面中央时,padding进来的是context;而当目标处于画面边缘时,padding进来的就是0了,这种信息的不同会影响定位和目标的判断。

2017

下面是VOT2017在隐藏数据集上的排名结果。

ECO

ECO(高效卷积算子)主要是为了解决C-COT速度慢的问题。从参数降维、样本分组和更新策略三个角度对其改进,在不影响算法精确度的同时,将算法速度提高了一个数量级。
为了减少模型参数,考虑到在C-COT中许多滤波器的能量(可理解为贡献)小得几乎可以忽略,因此ECO使用了一个较小的滤波器子集,且原来的滤波器都可以用这个子集中的滤波器线性组合表示(即乘上一个行数为高维数、列数为低维数的矩阵)。子集的选取方法就是简单的选取能量高于某一阈值的滤波器,其效果类似于PCA。作者提出了一个因子化的卷积算子来学习这个子集,用PCA初始化,然后仅在第一帧有监督地优化这个降维矩阵,在之后的帧中直接使用,相比C-COT模型参数量大大降低,同时也减轻了计算和存储的负担。

为了减少样本数量,作者提出了一个紧凑的样本空间生成模型,采用高斯混合模型(GMM,可理解为当有多个聚类时用多个不同的高斯模型来表示更好)来合并相似样本。当GMM的聚类(component)数量超过阈值时,如果有权重低于阈值的component,则丢弃之;否则,就合并最近的两个component。如此就可以建立更具代表性和多样性的样本集,既保持样本之间的差异性,也减少了存储的样本数量。

此外,作者还提出了一种稀疏的更新策略,即每隔N帧(实验发现5帧左右最好)才更新一次参数。由于样本集是每帧更新的,这种稀疏更新策略并不会错过间隔期的样本变化信息。此外,这种方法的另一个好处就是把原本的用逐帧单独样本进行更新,变成了用连续几帧所采集的样本所构成的batch来进行批处理的更新,这样就减小了在某帧遮挡、突变时过拟合的可能性。由此,稀疏更新策略不但提高了算法速度,而且提高了算法的稳定性。

CREST

CREST提出了将DCF构建成一层卷积神经网络,并且引入了残差学习来应对目标外观变化带来的模型退化。
考虑到之前的DCF系列没有发挥端到端训练的优势和空间卷积与相关滤波中循环输入点乘的相似性,作者用一层卷积神经网络来代替DCF的作用,这不仅使得模型能够通过反向传播训练,同时因为没有使用循环移位,避免了边界效应。
由于上述一层网络难以达成在多种情况下网络输出和ground truth的一致(模型复杂度较低易受干扰),而若使用多层网络很可能会导致模型退化(我理解为过拟合导致的),作者引入空间残差和时间残差。设我们希望最佳的输出为$H(x)$,而上述单层网络的输出是$F_{B}(x)$,为了补足某些时候(尤其是复杂情况下)单层网络的输出与希望最佳的输出之间的差距,引入残差项$F_{R}(x)=H(x)-F_{B}(x)$。在训练时,$F_{B}(x)$和$F_{R}(x)$中的参数一起训练,使得遇到特殊情况(遮挡、运动模糊等)时,$F_{R}(x)$能够补足纠正$F_{B}(x)$不稳定的响应结果。
空间残差和单层网络都是利用当前帧作为输入,考虑到空间残差有时候也会失效,作者又引入了把初始帧作为输入的时间残差,最终表达式如下:

注意:论文中第一项为$F_{R}(X_{t})$,可能有误,为此我作了修改。

另外,CREST采用当前帧最大响应尺度和上一帧尺度加权求和的方法来决定当前帧的最终预测尺度,从而使尺度能够平滑地变化。

LMCF

LMCF借鉴了KCF的循环特征图、Struck的结构化SVM,使用相关滤波在频域加速计算,从而解决了之前结构化SVM系列算法(Struck、DLSSVM)的速度问题。
在前向追踪时,LMCF考虑到画面中相似物体的干扰,提出了一种多峰值的目标跟踪算法(Multimodal Target Tracking),即对高于某一阈值的响应峰值做二次检测,把response map和一个用于筛选的二值矩阵作点乘,相当于把不是峰值的位置滤为0。对于通过筛选的峰值,以每个峰值为中心提取patch并用之前的方法再计算一遍峰值,取此时最大的峰值作为结果。
在模型更新时,LMCF提出了一种高置信度的更新策略(High-confidence Update),由于LMCF主要关注的是实时性,所以希望在算法简单的情况下能够减少失误。在传统的方法中,一般是当最大响应的峰值高于某一个阈值时(认为没跟丢目标),就对模型进行更新;否则若没有响应值超过峰值,就不对模型进行更新。而该工作的实验发现,当目标被遮挡时,响应图会震荡得非常厉害(存在多个较大的峰值),但同时最大响应的峰值仍旧会很高,这就会指导模型进行错误的更新并导致最后跟丢目标。于是作者提出了一个APCE值,定义如下。

只有当最大响应的峰值比较明确,即远超response map中的其他的响应时,APCE值才会比较大。因此LMCF仅当最大峰值和APCE超过阈值时才允许对模型进行参数和尺度模型的更新。

DeepLMCF

同LMCF,不同之处是使用了CNN特征。

MCPF

MCPF结合多任务相关滤波器(MCF)和粒子滤波器,这里的多任务相关滤波器指的是利用了多种特征滤波器之间的相关性。作者对K种特征,定义了参数$z_{k}$去选择具有判别力的训练样本。作者发现,各个特征中的$z_{k}$往往会选择具有相同循环移位的样本,因此不同的$z_{k}$应该具有相似性和一致性。为此,作者在损失函数中增加了矩阵Z的混和范数。
考虑到粒子滤波通过密集采样来覆盖状态空间中的所有状态,这会大大增加计算量,而且并不能保证很好地包括目标物体在一些情况下的状态。因此作者利用MCF对每个采样的粒子进行引导,使其更接近目标的状态分布。这样就可以在提升效果的同时每次采集较少的粒子,从而提高计算效率。另一方面,粒子滤波的密集采样能够在目标尺度发生变化时覆盖状态空间,这就解决了单一相关滤波器的尺度问题。
算法的流程分为四步:
首先,使用转移模型生成粒子并且重采样。
然后,使用MCF对粒子进行微调,使其转移到比较合适的位置。
接着,利用响应更新MCF的参数。
最后,通过求样本均值问题来决定目标的状态,也就是位置等参数。
可见,这里相关滤波仅起到指导的作用,最后的决策由粒子滤波器做出。
顺便一提,MCPF使用了Accelerated Proximal Gradient来解决这里不可微分的凸优化问题(含有范数)。

CFNet

CFNet结合相关滤波的高效性和CNN的判别力,考虑到端到端训练的优势,从理论对相关滤波在CNN中的应用进行了推导,并将相关滤波改写成可微分的神经网络层,将特征提取网络整合到一起以实现端到端优化,从而训练与相关滤波器相匹配的卷积特征。
CFNet采用孪生网络的架构,训练样本(这里指用来匹配的模板)和测试样本(搜索的图像区域)通过一个相同的网络,然后只将训练样本做相关滤波操作,形成一个对变化有鲁棒性的模板。为了抑制边界效应,作者施加了余弦窗并在之后又对训练样本进行了裁剪。
在对比实验中作者发现仅使用一层卷积层时CFNet相比Baseline+CF效果提升最显著,对此作者的解释是可以把相关滤波层理解为测试时的先验知识编码,当获得足够的数据和容量时(增加CNN层数时),这个先验知识就会变得冗余甚至是过度限制。

2018

下面是VOT2018 short-term的排名结果。

STRCF

STRCF(时空正则相关滤波器)主要针对SRDCF的速度做出改进,同时在精度上也有很好的提高。作者发现SRDCF速度很慢的两个原因是:每次对多张图片进行训练打破了循环矩阵的结构,从而无法发挥循环矩阵的计算优势;巨大的线性方程组和Gauss-Seidel迭代法没有闭式解,效率较低。对此,STRCF提出了引入时间正则和ADMM算法。
受online Passive-Aggressive learning的启发,STRCF在SRDCF空间正则的基础上引入了时间正则。我们可以对比两者的回归求解公式具体来看一下。
SRDCF:

STRCF:

这里的$w$表示空间正则化矩阵,越靠近边缘值越大;$f$表示相关滤波器,$f_{t-1}$表示的是$t-1$帧时的滤波器。
忽略每项之前的常数系数,我们可以看到两式的第二项是一样的,也就是STRCF保留了SRDCF的空间正则来抑制边界效应;在第一项中,STRCF没有对过去的每一帧进行求和来训练,这就减小了计算量;同时STRCF加入了第三项时间正则,使得新得到的滤波器与之前的滤波器之间的变化尽可能小,相当于保留了之前的信息。
这么做有两点好处:首先,STRCF可以看作SRDCF的一个合理近似,能很好地发挥后者同样的作用;此外,由于时间正则的引入,使得STRCF不易于在当前帧上过拟合,在遇到遮挡或者超出画面等问题时,STRCF能很好地保持与之前滤波器的相似度从而降低了跟踪器完全跟丢到另一个物体上去的可能,这一定程度上提高了STRCF的精度。

此外,ADMM算法的引入使得最优化求解问题有了闭式解,这比Gauss-Seidel迭代法用稀疏矩阵求解要快得多。得益于SRDCF的凸性,ADMM也能收敛到全局最优点。

UPDT

UPDT区别对待深度特征和浅层特征,主要考虑的是缺少数据和深层卷积在增加语义的同时降低分辨率这两个问题。作者分析了数据增强(flip,rotation,shift,blur,dropout)和鲁棒性训练(也就是tracker应对各种复杂场景和恢复的能力,可以通过扩大正样本的采样范围来训练)对deep feature和shallow feature分别的影响,发现deep feature能通过数据增强来提升效果,同时deep feature主打的是鲁棒性而不是精度;相反,shallow feature经数据增强后反而降低了效果,但同时它能够很好地保证精度。因此,作者得出了深度模型和浅层模型应该独立训练,最后再融合的方案。
作者在文中还定义了Prediction Quality Measure,考虑了精度和鲁棒性,精度用响应分数的锋利程度(sharpness)来体现,鲁棒性则用响应值的幅度来表示,幅度越高表明tracker越确信跟踪的目标,也就是鲁棒性越高。关于具体公式的推导和分析,以及Prediction Quality Measure在预测过程中的具体使用可以看一看原文。

ACT

ACT使用了强化学习,构建了由Actor和Critic组成的学习框架。离线训练时,通过Critic指导Actor进行强化学习;在线跟踪时,使用Actor来定位,Critic进行验证使得tracker更加鲁棒。不同于之前的搜索方案(随机采样或者通过一系列分离的action来定位),ACT希望的是搜索一步到位。这步最优的action也就是离线强化学习所关注的行动,而强化学习的状态由输入到网络中bounding box中框出的图片定义,奖励值根据IoU来定义。
在训练的过程中,由于action space比较大,因此要获得一个正奖励比较困难(随机采取action的话IoU恰好高于阈值的可能性较小)。因此作者利用了第一帧的信息来初始化Actor以适应新的环境。同样的,由于巨大的action space,原本DDPG方法中的噪声引入就不适合跟踪任务了,因此在训练前期,Actor采取的行动以某种概率被一种专家决策所替代。随着训练的进行,Actor越来越强大,这时就逐渐减弱专家决策的指导作用。
在跟踪的初始帧,作者首先在第一帧提供的ground truth周围采集多个样本。然后使用Actor对这些样本作action,根据得分对Actor进行一次微调;对Critic,根据打分和ground truth也进行一次初始化训练。
在之后的跟踪过程中,若Critic的给分大于0,则采用Actor的输出一步到位地预测下一帧的目标;否则,再使用Critic在上一帧周围采集的样本中选出最优作为目标,完成重定向。此外,可以认为Actor在离线训练时已经比较稳定了,因此在跟踪过程中只对Critic进行更新,且仅在Critic给分小于0(认为Critic没能很好地适应目标的变化)时,取前十帧的样本来更新Critic。

DRT

DRT引入了可靠性的概念,考虑到空间正则、掩模等抑制边界效应的方法都不能抑制bounding box内部的背景信息,同时这些方法会导致滤波器的权重倾向于集中在某些较小的区域(主要是中央的关键区域,我理解为边缘区域被抑制掉了,因此学习时自然不会去分配权重),作者认为这是不利于目标跟踪的(容易个别不可靠的区域被误导)。为此,作者提出了DRT,它主要是将滤波器分成了一个base filter和一个reliability term的element-wise product:

这里的base filter用于区分目标和背景;reliability term用于决定每片区域的reliability,由目标区域每一个patch的reliability值加权求和决定:

这里的$p$是对每一个patch的掩模,用于确定每个patch做相关操作的区域;$\beta$有上下界的限定,目的就是为了降低feature map中响应不平衡的影响,防止由于响应的集中而导致仅有一小块区域被关注。

需要最小化的目标方程包括三项:分类误差、局部一致性约束和滤波器参数$h$的二范数。分类误差就是与ground truth之间的损失函数,计算时需考虑可靠性;局部一致性约束用于减小循环样本中的每一个片段的响应差距,该项不受$\beta$即可靠性的影响,也就是说base filter在训练时依旧要保持对每个局部区域同样的关注度,使得base filter能独立于可靠性进行训练,这就避免了前面提到的滤波器在训练时边缘区域被抑制所造成权重集中的后果;滤波器参数$h$的二范数用于保证模型的简单程度,防止模型退化(可以理解为过拟合)。
由于只有当base filter的参数$h$和reliability的权重$\beta$有一项已知时,目标方程的最小化问题才是凸优化问题,因此作者采用了$h$、$\beta$交替训练的方法。
作者还使用权重逐帧退化的方式设计了一种简单的利用多帧信息的目标方程。借鉴ECO,DRT也采用了间隔几帧更新一次的稀疏更新方法和基于高斯混合模型的样本分组策略。类似DSST,DRT采用了先确定位置再计算多个尺度的响应的“两步”尺度估计方法。

MCCT

MCCT使用了多特征集成学习,在跟踪时对每一帧分别选用最合适的特征来做出决策。为了应对不同的场景,MCCT选择了low,middle,high三个层级的特征,并通过排列组合得出7种expert。尽管有些特征的鲁棒性明显差于三类特征的组合,但是它们提供的多样性对集成学习是至关重要的。

为了评估每个expert在每一帧的好坏以决定具体选用哪一个,作者提出了Expert Pair-Evaluation和Expert Self-Evaluation。
Expert Pair-Evaluation分为两项:在第一项中,作者认为一个expert的好坏可以通过它与其他expert的整体一致性来体现,于是首先计算了每个expert相对于其他6个expert在当前帧预测结果的一致性(通过重叠率来衡量)之和;此外,作者认为一个好的expert还必须是temporal stable的,因此他又计算了每个expert相对于其他6个expert在前几帧内预测趋势的一致性,这就可以防止因为在当前帧碰巧预测一致而导致之前一项的分值很好的情况,也保证了expert的可信度。最后两项结合得到Expert Pair-Evaluation。
在Expert Self-Evaluation中,作者认为路径的顺滑程度一定程度上能够体现每个expert的可靠程度。
最后将Expert Pair-Evaluation和Expert Self-Evaluation加权求和选出每帧最好的expert做出决策。
MCCT提出了一种peak-to-sidelobe ratio和鲁棒性的置信度分数来进行模型更新:

其中,$P_{mean}^{t}$是每个expert响应图peak-to-sidelobe ratio的平均,$R_{mean}^{t}$亦然。当$R_{mean}^{t}$比较低时,认为采集到了不可靠的样本(比如遮挡问题等)。为此,作者的模型更新策略是,当置信度分数$S^{t}$大于之前置信度均值时,采用正常学习率,否则,根据置信度算出一个较小的学习率以在一定程度上维持模型。
为了提升速度,每个expert之间共享了样本和RoI,最后MCCT的速度为7.8FPS,MCCT-H(没采用深度特征)的速度为44.8FPS。(作为参考,ECO的速度为15FPS)

LSART

LSART分析了深度特征中的空间信息,提出了两种互补的回归方式来使得跟踪更加鲁棒。
作者首先对比了CNN-based和KRR-based(核岭回归)两类tracker,认为它们各有利弊且是互补的。由于KRR的循环采样,目标的结构特征会被打破,对形变和遮挡问题效果不好,而CNN则能够很好地提取位置信息;相反,CNN庞大的参数量使得它容易过拟合,而KRR-based tracker就不会出现这样的问题。因此,若将两者结合(将热力图加权求和),就可以让KRR关注全局而让CNN关注较小、较精确的目标,进而达到更好的效果。
对于KRR,作者引入cross-patch similarity,将参数看作训练样本的加权求和,将响应项拆分成三个模块,这就方便把原本的迭代求解的方式分成三步在神经网络中来求解了。
对于CNN,考虑到形变和遮挡等问题会使得目标的一部分比其他区域更加重要,不同于以往在feature map上做文章,作者对卷积层的滤波器施加掩模,使得各个滤波器关注于不同的区域,在跟踪的过程中,这些掩模不做变化。此外,作者还提出了距离变换池化层用于评判输入feature map的可靠性。另外,作者设计了一种two-stream的训练网络,将空间正则的卷积层和距离变换池化层分开训练以防止过拟合,能够比较好的处理旋转问题。

SiamRPN

SiamRPN利用了Faster RCNN中的RPN,解决了之前深度学习跟踪算法没有domain specific(可理解为类间不区分)以及还需额外的尺度检测与在线微调的问题。RPN回归网络的引入,一方面提高了精度,另一方面回归过程代替多尺度检测,使得速度有所提升。
在在线跟踪时,SiamRPN将跟踪看作one-shot检测的问题,也就是用第一帧目标样本的信息来预测RPN网络中的参数,从而实现domain specific且不需要在线更新。作者把template分支和detection分支卷积视作类别信息在RPN网络上的embedding。

DaSiamRPN

DaSiamRPN在之前的孪生网络系列的基础上增加了distractor-aware,这里的distractor指的是在判别式方法中,不同于无语义信息易判别的背景,而存在一定的语义并对前景分割存在干扰的背景。这其中的一大原因是之前的训练集仅从同一个视频序列的不同帧中采样,造成了non-semantic的背景样本具有较大的比重而semantic的背景样本较少,这就弱化了模型准确判别前景的能力。此外,之前的孪生网络系列还存在不能在线更新和不进行全局搜索这两个问题。
首先,作者提出了三类样本选取方法来弥补传统采样的不足。考虑到视频数据集中类别缺乏和标注的难度,作者引入了ImageNet和COCO图像检测两个数据集,并把样本分成三类对tracker进行训练。

对于正样本对,其作用是提升tracker的泛化能力和回归精度;对于来自同一类别的样本对,其作用是让tracker更注重细粒度的表达方式,提升判别能力;对于来自不同类别的样本对,其作用是让tracker在遮挡、超出视野等情况下拥有更好的鲁棒性。
值得一提,作者发现motion pattern能很好地被浅层网络建模,因此在数据增强时还引入了运动模糊。
DaSiamRPN通过上述方法对数据做了增强,可是在跟踪特定目标时,还是很难将一般模型转化为特定视频域所用。考虑到上下文信息和时域信息可以提供特定目标的信息以增加tracker的判别能力,作者提出了一个distractor-aware module。具体来说,在上一帧中选择出的proposal中,通过非极大抑制处理,剩下的proposal中最大的就是目标,剩下的就是会产生误导的distractor;在当前帧,为了抑制这些distractor的干扰,可以减去这些distractor之前响应的加权和,减去之后还是最大的proposal就是我们要找的目标,其基本思想如下公式所示:

这里的$\alpha$是控制distractor影响大小的权重系数,作者又对上式进行调整,通过引入学习率使得该分类器在线可学习,这就无需利用反向传播更新网络参数,而通过微调一个分类器弥补了传统基于孪生网络的tracker不能在线更新的缺点。
此外,当认为目标跟丢时,DaSiamRPN会匀速扩大搜索范围,并且通过高效的bounding box回归来代替图像金字塔,这就通过一个简单的方法在应对长时跟踪目标消失问题时较之前基于孪生网络的tracker取得了一个进步。

Meta-Tracker

Meta-Tracker将元学习运用在了目标模型的初始化上。作者认为结合深度特征和在线学习的模型有两大困难,一是训练的样本不容易获得,二是大多数SOTA的tracker在训练阶段都需要花费大量的时间在初始化上面。
针对上面的难题,作者提出了一种在未来的帧上训练目标模型的思路,采用了基于预测梯度的策略学习方法获得普适性的初始化模型,使得跟踪模型自适应于后续帧特征的最佳梯度方向,从而在接收到第一帧时仅需一步迭代就能使参数快速收敛到合适的位置。这样做有三点好处,一是能使模型更加关注对后续的帧更有价值的特征,二是避免了在当前帧上过拟合,三是能够使初始化更快速。总而言之,就是能保证精度和鲁棒性。
考虑到上述方法在长序列或者目标在帧与帧之间变化不大时表现不佳(会偏离目标),这是因为Meta-Tracker一步到位的思想使得学习率会偏大。因此作者仅在模型初始化时采用学习到的学习率,在之后的跟踪过程中仍旧沿用原来版本的方式进行更新。这里原来的版本指的是CREST和MDNet,作者在这两个tracker的基础上改进出了MetaCREST和MetaSDNet。具体的改进和处理可以看一看我之前写过关于Meta-Tracker的文章。

DorT

DorT(Detect or Track)把跟踪看作一个连续决策的过程,它结合目标检测和目标跟踪两个领域内SOTA的结果,在孪生网络输出的结果上再添加一个小型的CNN网络作为scheduler来判断在下一帧是作检测还是作跟踪。

LADCF

LADCF针对DCF系列的边界效应和模型退化(后者主要是单帧独立学习和模型更新速率固定导致)的问题,提出了一种空间域特征选择和时间域约束结合的方法,并且使其能在低维流形中有效表示。

补充:流形学习的观点认为,我们所能观察到的数据实际上是由一个低维流形映射到高维空间上的。由于数据内部特征的限制,一些高维中的数据会产生维度上的冗余,实际上只需要比较低的维度就能唯一地表示。

掩模策略应用于目标跟踪时,仅将目标区域的参数激活。LADCF也运用了这个思想,对滤波器中的参数$\theta$作降维处理$\theta _{\phi }=diag(\phi )\theta$,这里$\phi$中的元素要么是0、要么是1,即不激活或者激活。不同于PCA和LLE,这种方法在降维的同时也保持了空间特性,不仅能加速求解,也能除去大部分干扰,使滤波器关注于目标部分从而可以使用更大的搜索域。
最后的目标函数如下:

可以看到,这里还包括与历史模型的正则项,减轻了滤波器退化。作者让$\lambda _{1}< < \lambda _{2}$,也就是让时间域上的一致性更加重要于特征的稀疏选取。

FlowTrack

不同于之前先把光流算好,FlowTrack是第一个把光流信息进行端到端训练的,这无疑提高了光流使用的精度。作者注意到之前的算法大都采用RGB特征也就是外观特征,且缺少对运动特征和帧与帧之间联系的利用,这就导致在部分遮挡和形变等情况下效果会变差。不同于把之前的几帧保留下来等时域方法,作者希望能把前几帧的特征直接补充到当前帧上融合成一个,而具备方向和速度信息的光流就成了一种比较合适的方法。为此,作者将光流引入孪生网络框架,使得仅使用外观特征的一些不足得到弥补。
作者的思想很巧妙,我以遮挡问题为例简述一下。作者的想法是当当前帧的目标有部分被遮挡时,我们可以根据光流的运动信息,把前几帧的特征映射过来对齐,通过插值补全当前帧的特征,简单来说可以理解为把当前帧缺的那块给补全。为了有效地选择补过来的特征,作者使用了空间注意力和时间注意力两种机制结合。空间注意力分别计算前几帧补过来的特征与当前帧的特征的相似度,根据相似度大小来分配特征在各个空间位置上的权重。但是由于最近的一帧总是与当前帧的特征最为相似,它的权重肯定是最大的,考虑到假如最近一帧由于遮挡等原因等导致特征质量下降而不适合分配最大的权重,因此还需要时间注意力来重新校准权重。具体来说,时间注意力作用在空间注意力的输出上,对于一般情况下的目标基本不会改变空间注意力的输出,而对遮挡等情况下的帧就会减小其在空间注意力中分配到的权重。

VITAL

VITAL针对正样本高度重叠无法捕获目标丰富的特征变化和正负样本不平衡的问题,采用对抗学习的思想,分别设计了生成网络和代价敏感损失来解决这两个问题。
这里的生成网络输出为一个作用于目标特征的掩模。通过对抗学习,该生成网络可以产生能保留目标特征中较为鲁棒的部分。其目的是对仅在个别帧判别力强的特征进行削弱,防止判别器过拟合于某个样本。个人理解,这里的判别力强的特征并不是指的能够很好地区分目标和背景的特征,而是指仅在某些帧出现比较独特,而在大多数情况下不存在于目标上的特征。
考虑到很容易被分类正确的负样本在训练过程中也会产生损失,然而我们并不希望网络关注这些损失,因为关注他们反而会使得网络性能变差。因此,为了避免很多负样本主导损失函数,作者采用高阶敏感损失,减小简单负样本的权重,这不但提升了精度,也加速了网络的收敛。

SA-Siam

SA-Siam考虑到语义信息和外观信息的互补关系,构建了两个并行的孪生网络。在外观网络(作者使用的是SiamFC)具有判别力的基础上,结合语义网络(作者使用了AlexNet的第4和第5层作为backbone)更泛化、更鲁棒的语义信息,使得tracker的效果更好。
外观网络的输入为目标和搜索区域,其框架跟SiamFC基本一致,这里就不细说了。
不同于外观网络,语义网络的输入为目标及其背景和搜索区域(两者一样大),作者直接使用了AlexNet提取高层的语义信息并不进行训练,因为假如训练了的话就跟外观网络接近了,而集成学习的思想就是被集成的各个模型之间的关联性应比较弱,这也是文中多次强调的。在提取高维语义信息之后,为了使得得到的语义特征更适合于跟踪任务,作者使用了1x1的卷积层作了一次特征的fusion。此外,考虑到对不同的追踪目标各个通道的重要性是不同的,作者还设计了一个通道注意力模块,具体而言,就是对每一个通道,先做最大池化变成3x3的网格,然后再通过一个多层感知机来决定每个通道的每个格子的重要性。由于通道注意力的选择也受目标周围背景的影响,因此最大池化的结果仅中间的格子是目标区域,周围的8个格子代表目标周围的背景信息。再次强调,在训练过程中,语义网络冻结语义特征提取的AlexNet,仅训练特征fusion和通道注意力模块。
根据上文的解释,同样的,为了保持语义网络和外观网络的弱关联性和互补性,两支网络独立训练,仅在测试时加权得到最后的相似度图作为预测依据。

2019

下面是VOT2019 short-term的排名结果。

GFS-DCF

GFS-DCF考虑到深度网络的高维通道存在许多冗余的信息,因此作者在时域和空间域之外,还考虑了通道维度的影响,在目标函数中使用了三个正则项。

注:建议结合上图来看接下来的分析。

对于空间域,作者将每个通道(也就是每个feature map)的对应点相连接,用范数约束,可以理解为提取那些在绝大多数特征图中都是最重要的特征的位置。

从通道角度,作者又把每一个通道作为一项来做约束,可以理解为提取那些特征比较重要的通道。

对于时域,作者使用了low-rank约束,这里的rank指的是矩阵的秩而不是排名,low-rank主要用于图像对齐(alignment),在文中的目标是最小化$rank(W_{t})-rank(W_{t-1})$,这里的$W_{t}$表示从1到t每个滤波器向量化后形成的矩阵。下面是作者最后修改后的正则项:

注:上述三项在实际目标函数中还要加上权重。

作者发现,空间正则对使用handcrafted特征的模型效果显著,而对使用CNN的模型(文中是ResNet)效果提升不大;相反,通道正则对使用handcrafted特征的模型效果不明显,而对使用CNN的模型效果显著。作者在文中解释认为由于深层CNN特征表示的语义信息丰富而缺少细粒度的信息,因此相比保留更多空间结构handcrafted特征,对深层CNN特征使用空间正则比较难以判别哪些位置的特征反应了目标位置的信息。此外,由于在训练过程中一些通道的权重下降到很小,也就是说模型本身就不怎么关注这些通道,因此使用通道正则在这里取得了比较明显的效果。

D3S

D3S考虑到BBox对目标的粗糙表示会影响性能以及视频分割任务中对背景干扰和长时视频不鲁棒的问题,提出了一种视频跟踪、视频分割互补的框架。

如上图所示,作者构造了用于分割的GIM和用于定位的GEM两个模块。
GIM使用初始帧的目标像素点构造目标的特征向量,使用初始帧目标周围的像素点构造背景的特征向量。在之后的图像中,每个像素点的目标相似度,定义为该点和每个目标的特征向量做相似度计算之后,最大的K个目标相似度的均值;同理,每个像素点的背景相似度,定义为该点和每个背景的特征向量做相似度计算之后,最大的K个背景相似度的均值。最后将目标相似度图和背景相似度图作softmax得到分割结果。该模块没考虑位置信息,用分割使得在应对剧烈形变的目标时能够取得较好的效果。
然而,GIM的分割结果对相似的物体或者背景的干扰并不鲁棒,因此我们希望能有鲁棒的位置信息来提升判别力。GEM就是直接使用一个DCF来找到最大响应值的位置,也就是目标的中心位置,然后以该中心为圆心,向周围每个像素点根据半径由大到小分配置信度。最后仅把GIM中得到的且由GEM分配的置信度高于阈值的目标像素点作为分割结果。
由于通过backbone的encode导致此时的输出分辨率较低,因此还需要作上采样。具体来讲,就是每次把输入扩大到两倍分辨率,通过两次卷积,然后加上backbone中对应分辨率的层。
由于预训练的backbone特征缺少精细的划分,因此作者在初始帧先进行降维的训练。具体来讲,就是首先将预训练网络通过1x1的卷积来降维,再通过一层3x3的卷积,从而达到调整网络参数使得分支划分得到最佳的目的。
由于输出是二值掩模,因此作者还对使用BBox的目标跟踪问题作了变换处理。首先,在初始帧,除了进行降维和DCF的训练,作者还先把BBox内部的像素点视为目标点,把BBox外4倍区域的像素点视为背景点,用D3S在第一帧上迭代(文中说只迭代1次就可以了)以产生比BBox更细致的分割,将最后分割出的目标点和其周围的背景点用于构造特征向量,在之后的跟踪过程中保持不变并用于GIM模块。此外,在输出时,作者先用椭圆去近似掩模,然后根据长轴和短轴来确定BBox。由于椭圆的确定遗漏的背景信息(仅考虑怎么把mask包进来)从而导致BBox偏大,因此作者提出了一种考虑背景信息的方法,对长轴进行微调来确定最终的BBox。

GlobalTrack

GlobalTrack关注的是long-term tracking的问题。我们知道,在目标跟踪问题中,为了更好的利用前一帧甚至前几帧的信息,往往会对模型做很多假设,包括目标的运动、位置变化、尺度变化(假设平滑变化等等),而这些假设并不能很好地处理所有的情况(比如位置或尺度突变、目标消失、短时跟踪失败等),由此产生了模型的累计误差。而在长时跟踪问题中,这样的累计误差往往会使得后期的目标跟踪结果差很多。
基于上述考虑,GlobalTrack根据长时跟踪的特点,把跟踪看作在每一帧作全局检测的问题,设计了一种没有运动模型、没有在线学习、没有位置估计、没有尺度平滑的无累积误差的baseline,其基本框架如下图所示。

受Faster RCNN启发,GlobalTrack也是基于two-stage的框架。其下面的一条和Faster RCNN基本一致,由于Faster RCNN是目标检测类算法,其目的是在图像中框出所有物体并分类。而目标跟踪仅需要目标,因此作者用添加了上面一条query-specific的引导。为了简单起见,作者把query的RoI看成kxk的方型特征,在第一个feature modulation中,作者把RoI特征卷积成1x1的卷积核,然后再和通过backbone的搜索图像特征做卷积相关操作,得到query-specific的候选框;在第二个feature modulation,作者把RoI特征与每个候选框作哈达玛积(也就是简单地将两个尺寸相同的矩阵的对应位置作乘积),由此来改进标签置信度和BBox的预测。
在训练时,作者取多对图像对,其中每一对图像都共同含有M个相同的实例,作者用每对图像来相互预测每对各自的M个实例,使总的损失达到最小以进行训练。
在测试时,作者简单地把第一帧作为query,之后的每一帧都视为独立的全局检测,直接取得分最高的BBox作为结果,如此就不存在依赖相邻帧带来的累计误差了,因此作者认为视频长度越长,GlobalTrack的表现就越突出。

SPSTracker

SPSTracker针对目标周围噪声引起的响应(sub-peak)导致模型漂移,以及由多尺度样本加权得到的特征图响应最大值和目标真正的几何中心不一致的问题,提出了BRT和PRP两个模块。作者使用的是ATOM的框架,两个模块在框架中的使用方式如下图所示。

这里的BRT其实就是简单的将远离目标中心的点置为0,其目的是减小响应图的方差,使得响应图尽可能地呈现单峰响应的形状,此外也起到了抑制边界效应的效果。PRP其实就是作者新设计的一个池化层,该池化层把每一像素点的值设为该像素点所在列所有像素点中的最大值和所在行所有像素点的最大值之和,从而使响应值能更加靠近目标的几何中心,从而能够更好地使用多尺度样本。作者对搜索区域使用BRT,然后将通过分类器的置信度图通过一个PRP构造的残差模块,从而达到抑制sub-peak的目的。

SiamRCNN

SiamRCNN发现重检测很容易受到干扰物的影响从而产生模型漂移,从难例挖掘和运动轨迹动态规划两个角度入手,设计了一个利用第一帧和前一帧为模板的孪生网络检测结构,在短时跟踪评价上能与之前SOTA的算法持平,在长时跟踪评价上有非常显著的进步。注意,这里的追踪轨迹不仅追踪目标,同时也追踪所有的distractor。

SiamRCNN可以说综合了许多前人的成果。为了收集更多难例,作者在不同的视频上根据嵌入网络上的距离来获得更多负样本。接下来,我结合上面的网络结构图来简述一下我对该算法的理解。
首先,作者使用RPN网络获得了类别无关的proposal,然后和第一帧结合,通过一个与Faster RCNN前半段相似的Re-Detection Head。接着,通过3个级联的RCNN获得回归后的bounding box(这里借鉴了Cascade RCNN,其认为高质量的proposal能够产生更好的结果,3级RCNN的IoU阈值设定逐级升高)。随后,作者将此时获得的bounding box与前一帧所有的检测结果两两组合(也就是即检测目标也检测干扰物),再次输入到之前相同的重检测结构中。最后对得到的所有结果进行跟踪轨迹动态规划。
在轨迹动态规划模块,作者计算轨迹与检测结果的相似度,用不具有不确定性的检测结果去延续之前的若干条轨迹。这里的不确定性定义为:对这条跟踪轨迹,有其他的检测结果和本结果具有相当的相似度;对检测结果,有其他的跟踪轨迹和本轨迹具有相当的相似度。对于不确定的检测结果,作者让它们开启新的轨迹。对于没有确定检测结果的轨迹则暂时中断,其中也包括含有初始帧的轨迹,当它中断时可认为目标消失了。


研究趋势

以下是我对近几年来目标跟踪领域各种算法主流的研究趋势和发展方向的一个浅析,个人思考,多多指教。

注:其实近几年还出现了一些其他的关注方向,由于不是主流、目前关注较少、本人学识不够等原因,在此不做列举。

信息提取

深度特征

早期的目标跟踪算法主要在handcrafted特征方面进行探索和改进,以2012年AlexNet问世为节点,深度特征开始被引入目标跟踪领域。
我们知道,在现实场景中,物体是在三维的运动场中移动的。而视频或图像序列都是二维的信息,这其实是一些难题的根本原因之一。一个比较极端的例子就是理发店门前经常会出现的旋转柱,如果单纯地从二维角度来看,柱子是向上运动的,可在实际的运动场中柱子是横向运动的,观测和实际的运动方向是完全垂直的。

因此,为了能够更好地跟踪目标,我们需要提取尽可能好的特征,此外最好能从视频或图像序列中学到更多丰富的信息(尤其是含语义的)。值得注意的一点是,在港科大王乃岩博士2015年所做的ablation experiments中(详见参考文献[2]),发现特征提取是影响tracker效果最重要的因素。
考虑到精度是保证目标跟踪鲁棒性的重要因素,不同于一些其他的计算机视觉任务,目标跟踪领域的深度算法比较强调结合与充分利用浅层网络的高分辨率信息。

时域和空间域结合

可以说,在目标跟踪的相关算法中,空间正则指的就是抑制边界效应。由于CNN能够在学习的过程中能够产生对样本中各个区域有区分的关注度,因此可以不考虑边界效应。对边界效应的处理主要是在相关滤波类等需要循环移位的算法中出现。
事实上,目标跟踪这一个任务本身就在利用时域信息,因为预测下一帧肯定需要上一帧的信息,然而仅仅利用上一帧的信息往往是不够的,充分的利用时域信息在正则或者辅助记忆方面都可以取得一定的效果。

学习方式

结合语义分割的多任务学习

由于bounding box粗糙的对目标的标注表示使得有不少冗余的背景信息进入template,此外bounding box对平面内旋转等场景不鲁棒,这些都会导致模型的退化。早期也有许多算法考虑到了分割,但主要是对目标进行分块(part-based)而不是语义分割。由于跟踪的目标是有具体形状的且是一起运动的,同时判别式方法正是要分离除目标以外的物体,因此近年来有不少算法结合语义分割,通过多任务学习来进一步提高tracker的效果。具体来说,就是引入掩模(mask)来识别预测物体,最后在转化成bounding box作为结果。

元学习

实际上,目标跟踪这一个任务本身的特性就决定了它与元学习的思想有共通之处。元学习主要针对的是两个问题:在少样本学习的情况下对样本的利用效率比较低;当进行一个新的任务时对之前学到的经验的可移植性差,我觉得这里的新任务可以指从分类到跟踪这样类别之间的转换,也可以指在不同的视频序列上训练和测试这样“域”之间的转换。
当深度特征兴起之后,目标跟踪中的许多算法都选择迁移目标分类任务中的一些预训练模型来提取特征,这种迁移学习其实就包含了元学习的思想。MDNet将每个视频看做一个域,在测试时新建一个域但同时保留了之前训练时在其他域上学到的经验,既能够更快更好地在新的视频序列上学习也避免了过拟合。孪生网络实际上也是元学习领域一种比较常用的结构,它学习了如何去学习输入之间的相似度。

其他关注点

样本采集

样本采集主要包括样本的数量、样本的有效性、正负样本和难易样本的平衡性。

防止过拟合

每帧获取的少量信息和目标的意外变化导致信息的丢失,使得过拟合问题成为目标跟踪任务中一个比较重要的关注点,下面是一些比较常见的方法:

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>看了一寒假的目标跟踪,一直想将自己学习到的内容整理归纳一下,迟迟没动笔(其实是打字,但说迟迟没打字比较难听哈哈)。如今快要开学了,决定还是积累一 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>看了一寒假的目标跟踪,一直想将自己学习到的内容整理归纳一下,迟迟没动笔(其实是打字,但说迟迟没打字比较难听哈哈)。如今快要开学了,决定还是积累一 @@ -221,15 +221,15 @@ https://gsy00517.github.io/vot-toolkit20200215185238/ 2020-02-15T10:52:38.000Z - 2020-05-02T14:49:03.063Z + 2020-05-03T01:33:04.605Z -

今天终于把vot toolkit给搞好了,能够跑通NCC了,简直不要太开心。先是在windows下问题比较多,后来换到ubuntu上,的确问题少了很多,也方便了很多。但是每次执行run_experiments或者run_test的时候,会出现如下报错:

1
2
3
4
5
6
7
8
9
10
Initializing workspace ...
Verifying native components ...
Testing TraX protocol support for tracker NCC.
Tracker execution interrupted: Unable to establish connection.
TraX support not detected.
错误使用 tracker_load (line 127)
Tracker has not passed the TraX support test.

出错 run_test (line 8)
tracker = tracker_load('NCC');

于是我打印了厚厚一摞的TraX Documentation,照着一些步骤作了一遍可依然没有效果,最后终于在vot toolkit的gitHubissue中找到了解决方法。

References

电子文献:
https://github.com/votchallenge/vot-toolkit/issues/201
https://github.com/votchallenge/vot-toolkit/issues/216
http://votchallenge.net/howto/perfeval.html


solution

首先说明,我的运行环境为ubuntu18.04与matlab2018b。
vot-toolkit/tracker/tracker_run.m文件中找到如下代码(大约在30至40行左右)。

1
2
3
4
% Hint to tracker that it should use trax
environment.TRAX = '1';

connection = 'standard';

修改为:

1
2
3
4
% Hint to tracker that it should use trax
environment.TRAX = '1';

connection = 'socket';

再次执行run_experiments或者run_test即可。
其实不是很清楚为什么work了,根据这段代码之后的条件判断语句可知在windows下必须使用socket连接。

注:由于这期间我也做了其他的一些调整,不保证直接修改上述代码之后一定能够成功。请先按上面所说修改,如果成功,那么接下来的部分可以跳过。

  1. 如果仅下载了vot toolkit直接执行的话,它会自动为你下载TraX。这里应该没有问题,如果担心的话可以自己去github下载TraX,然后复制到vot-toolkit/native/目录下(没有的话创建一个)。
  2. 来到vot-toolkit/native/trax目录下,接下来都是终端操作。
    • mkdir build
    • cd build
    • make ..(请确保已经安装好cmake)
    • sudo make install(不加sudo会出现权限问题)
  3. 如果完成上面之后,执行还有问题,可能是路径没有设置对。打开vot-toolkit/workspace/tracker_XXX.m(XXX是你创建工作区时设置的跟踪算法的名称)。找到最后一行:% tracker_linkpath = {}; % A cell array of custom library directories used by the tracker executable (optional)
    去掉前面的注释符,添加路径tracker_linkpath = {'absolute_path/trax/build'};(记得修改这里的absolute_path)。
    据TraX的作者所说,TraX出错一般不是vot toolkit本身的问题,如果你使用的是其他的算法,请确保该算法的.m或者.py文件和vot.m或者vot.py文件处在同一个目录下。如果没有,可到vot-toolkit/tracker/examples中的matlab或者python目录下复制。

TraX

折腾了半天,总得知道这个TraX是个什么东西,根据TraX文档中所说:TraX stands for visual Tracking eXchange, the protocol was designed to make development and testing of visual tracking algorithms simpler and faster.
其实我当初折腾的时候想法是:我不要simpler and faster,我现在只想跑通哈哈哈哈哈。


NCC

一般使用vot toolkit都会根据官方文档先跑一下NCC来看看有没有设置好。NCC(归一化互相关)是一种基于统计学计算两组样本数据相关性的算法,比较老,性能可想而知。


sequences

我和我的同学在运行的时候都卡在了下载数据集那里,尽管在下载,但是网络下行却一直显示为0。询问一位计科大佬之后知道在maltab中下载数据集是基本没有速度的,所以一般的建议是使用别人网盘中下载好的数据集,我已经上传到我的百度网盘,可通过此链接用提取码0glq下载。

注:数据集已经压缩成zip文件,但大小仍有1.67GB。

下载解压之后,打开可以看到一个list文本文件和许多序列文件夹(一个序列一个文件夹),可以通过修改list文本文件来确定要读取并进行实验的序列。

注:
执行run_test可以选择list文本文件中列出的序列,选择对应序号之后可以看到按帧实验的情况。
执行run_experiments会执行list文件中列出的所有序列,可以ctrl+C暂停,之后再次执行run_experiments会从上一次暂停的地方开始执行。done之后可以执行run_pack生成可以提交至VOT challenge的archive档案。

]]>
+

今天终于把vot toolkit给搞好了,能够跑通NCC了,简直不要太开心。先是在windows下问题比较多,后来换到ubuntu上,的确问题少了很多,也方便了很多。但是每次执行run_experiments或者run_test的时候,会出现如下报错:

1
2
3
4
5
6
7
8
9
10
Initializing workspace ...
Verifying native components ...
Testing TraX protocol support for tracker NCC.
Tracker execution interrupted: Unable to establish connection.
TraX support not detected.
错误使用 tracker_load (line 127)
Tracker has not passed the TraX support test.

出错 run_test (line 8)
tracker = tracker_load('NCC');

于是我打印了厚厚一摞的TraX Documentation,照着一些步骤作了一遍可依然没有效果,最后终于在vot toolkit的gitHubissue中找到了解决方法。

References

电子文献:
https://github.com/votchallenge/vot-toolkit/issues/201
https://github.com/votchallenge/vot-toolkit/issues/216
http://votchallenge.net/howto/perfeval.html


solution

首先说明,我的运行环境为ubuntu18.04与matlab2018b。
vot-toolkit/tracker/tracker_run.m文件中找到如下代码(大约在30至40行左右)。

1
2
3
4
% Hint to tracker that it should use trax
environment.TRAX = '1';

connection = 'standard';

修改为:

1
2
3
4
% Hint to tracker that it should use trax
environment.TRAX = '1';

connection = 'socket';

再次执行run_experiments或者run_test即可。
其实不是很清楚为什么work了,根据这段代码之后的条件判断语句可知在windows下必须使用socket连接。

注:由于这期间我也做了其他的一些调整,不保证直接修改上述代码之后一定能够成功。请先按上面所说修改,如果成功,那么接下来的部分可以跳过。

  1. 如果仅下载了vot toolkit直接执行的话,它会自动为你下载TraX。这里应该没有问题,如果担心的话可以自己去github下载TraX,然后复制到vot-toolkit/native/目录下(没有的话创建一个)。
  2. 来到vot-toolkit/native/trax目录下,接下来都是终端操作。
    • mkdir build
    • cd build
    • make ..(请确保已经安装好cmake)
    • sudo make install(不加sudo会出现权限问题)
  3. 如果完成上面之后,执行还有问题,可能是路径没有设置对。打开vot-toolkit/workspace/tracker_XXX.m(XXX是你创建工作区时设置的跟踪算法的名称)。找到最后一行:% tracker_linkpath = {}; % A cell array of custom library directories used by the tracker executable (optional)
    去掉前面的注释符,添加路径tracker_linkpath = {'absolute_path/trax/build'};(记得修改这里的absolute_path)。
    据TraX的作者所说,TraX出错一般不是vot toolkit本身的问题,如果你使用的是其他的算法,请确保该算法的.m或者.py文件和vot.m或者vot.py文件处在同一个目录下。如果没有,可到vot-toolkit/tracker/examples中的matlab或者python目录下复制。

TraX

折腾了半天,总得知道这个TraX是个什么东西,根据TraX文档中所说:TraX stands for visual Tracking eXchange, the protocol was designed to make development and testing of visual tracking algorithms simpler and faster.
其实我当初折腾的时候想法是:我不要simpler and faster,我现在只想跑通哈哈哈哈哈。


NCC

一般使用vot toolkit都会根据官方文档先跑一下NCC来看看有没有设置好。NCC(归一化互相关)是一种基于统计学计算两组样本数据相关性的算法,比较老,性能可想而知。


sequences

我和我的同学在运行的时候都卡在了下载数据集那里,尽管在下载,但是网络下行却一直显示为0。询问一位计科大佬之后知道在maltab中下载数据集是基本没有速度的,所以一般的建议是使用别人网盘中下载好的数据集,我已经上传到我的百度网盘,可通过此链接用提取码0glq下载。

注:数据集已经压缩成zip文件,但大小仍有1.67GB。

下载解压之后,打开可以看到一个list文本文件和许多序列文件夹(一个序列一个文件夹),可以通过修改list文本文件来确定要读取并进行实验的序列。

注:
执行run_test可以选择list文本文件中列出的序列,选择对应序号之后可以看到按帧实验的情况。
执行run_experiments会执行list文件中列出的所有序列,可以ctrl+C暂停,之后再次执行run_experiments会从上一次暂停的地方开始执行。done之后可以执行run_pack生成可以提交至VOT challenge的archive档案。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>今天终于把vot toolkit给搞好了,能够跑通NCC了,简直不要太开心。先是在windows下问题比较多,后来换到ubuntu上,的确问题少 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>今天终于把vot toolkit给搞好了,能够跑通NCC了,简直不要太开心。先是在windows下问题比较多,后来换到ubuntu上,的确问题少 @@ -249,13 +249,13 @@ 2020-02-14T23:19:15.000Z 2020-02-15T23:14:19.130Z -

今天在公众号上看到一篇综述论文的翻译,该论文列举出了近年来深度学习的一些重要研究成果,从方法、架构,以及正则化、优化技术方面进行概述。看完之后感觉对目前学术图景有了一次基本的认识,于是决定搬过来,对之后参考文献查找也能起到很大的帮助。
之前也分享过一篇deep-learning笔记:一篇非常经典的论文——NatureDeepReview,可以一起看一下。
觉得阅读英文版更好,或者看完后想寻找对应参考文献的读者可以直接去论文地址下载原文。


Abstract

深度学习是机器学习和人工智能研究的最新趋势之一。它也是当今最流行的科学研究趋势之一。深度学习方法为计算机视觉和机器学习带来了革命性的进步。新的深度学习技术正在不断诞生,超越最先进的机器学习甚至是现有的深度学习技术。近年来,全世界在这一领域取得了许多重大突破。由于深度学习正快度发展,导致了它的进展很难被跟进,特别是对于新的研究者。在本文中,我们将简要讨论近年来关于深度学习的最新进展。


Introduction

“深度学习”(DL)一词最初在1986年被引入机器学习(ML),后来在2000年时被用于人工神经网络(ANN)。深度学习方法由多个层组成,以学习具有多个抽象层次的数据特征。DL方法允许计算机通过相对简单的概念来学习复杂的概念。对于人工神经网络(ANN),深度学习(DL)(也称为分层学习(Hierarchical Learning))是指在多个计算阶段中精确地分配信用,以转换网络中的聚合激活。为了学习复杂的功能,深度架构被用于多个抽象层次,即非线性操作;例如 ANNs,具有许多隐藏层。用准确的话总结就是,深度学习是机器学习的一个子领域,它使用了多层次的非线性信息处理和抽象,用于有监督或无监督的特征学习、表示、分类和模式识别。
深度学习即表征学习是机器学习的一个分支或子领域,大多数人认为近代深度学习方法是从2006年开始发展起来的。本文是关于最新的深度学习技术的综述,主要推荐给即将涉足该领域的研究者。本文包括DL的基本思想、主要方法、最新进展以及应用。
综述论文是非常有益的,特别是对某一特定领域的新研究人员。一个研究领域如果在不久的将来及相关应用领域中有很大的价值,那通常很难被实时跟踪到最新进展。现在,科学研究是一个很有吸引力的职业,因为知识和教育比以往任何时候都更容易分享和获得。对于一种技术研究的趋势来说,唯一正常的假设是它会在各个方面有很多的改进。几年前对某个领域的概述,现在可能已经过时了。
考虑到近年来深度学习的普及和推广,我们简要概述了深度学习和神经网络(NN),以及它的主要进展和几年来的重大突破。我们希望这篇文章将帮助许多新手研究者在这一领域全面了解最近的深度学习的研究和技术,并引导他们以正确的方式开始。同时,我们希望通过这项工作,向这个时代的顶级DL和ANN研究者们致敬:Geoffrey Hinton(Hinton)、Juergen Schmidhuber(Schmidhuber)、Yann LeCun(LeCun)、Yoshua Bengio(Bengio)和许多其他研究学者,他们的研究构建了现代人工智能(AI)。跟进他们的工作,以追踪当前最佳的DL和ML研究进展对我们来说也至关重要。
在本论文中,我们首先简述过去的研究论文,对深度学习的模型和方法进行研究。然后,我们将开始描述这一领域的最新进展。我们将讨论深度学习(DL)方法、深度架构(即深度神经网络(DNN))和深度生成模型(DGM),其次是重要的正则化和优化方法。此外,用两个简短的部分对于开源的 DL 框架和重要的 DL 应用进行总结。我们将在最后两个章节(即讨论和结论)中讨论深入学习的现状和未来。


Related works

在过去的几年中,有许多关于深度学习的综述论文。他们以很好的方式描述了DL方法、方法论以及它们的应用和未来研究方向。这里,我们简要介绍一些关于深度学习的优秀综述论文。
Young等人(2017)讨论了DL模型和架构,主要用于自然语言处理(NLP)。他们在不同的NLP领域中展示了DL应用,比较了DL模型,并讨论了可能的未来趋势。
Zhang等人(2017)讨论了用于前端和后端语音识别系统的当前最佳深度学习技术。
Zhu等人(2017)综述了DL遥感技术的最新进展。他们还讨论了开源的DL框架和其他深度学习的技术细节。
Wang等人(2017)以时间顺序的方式描述了深度学习模型的演变。该短文简要介绍了模型,以及在DL研究中的突破。该文以进化的方式来了解深度学习的起源,并对神经网络的优化和未来的研究做了解读。
Goodfellow等人(2016)详细讨论了深度网络和生成模型,从机器学习(ML)基础知识、深度架构的优缺点出发,对近年来的DL研究和应用进行了总结。
LeCun等人(2015)从卷积神经网络(CNN)和递归神经网络(RNN)概述了深度学习(DL)模型。他们从表征学习的角度描述了DL,展示了DL技术如何工作、如何在各种应用中成功使用、以及如何对预测未来进行基于无监督学习(UL)的学习。同时他们还指出了DL在文献目录中的主要进展。
Schmidhuber(2015)从CNN、RNN和深度强化学习(RL)对深度学习做了一个概述。他强调了序列处理的RNN,同时指出基本DL和NN的局限性,以及改进它们的技巧。
Nielsen(2015)用代码和例子描述了神经网络的细节。他还在一定程度上讨论了深度神经网络和深度学习。
Schmidhuber(2014)讨论了基于时间序列的神经网络、采用机器学习方法进行分类,以及在神经网络中使用深度学习的历史和进展。
Deng和Yu(2014)描述了深度学习类别和技术,以及DL在几个领域的应用。
Bengio(2013)从表征学习的角度简要概述了DL算法,即监督和无监督网络、优化和训练模型。他聚焦于深度学习的许多挑战,例如:为更大的模型和数据扩展算法,减少优化困难,设计有效的缩放方法等。
Bengio等人(2013)讨论了表征和特征学习即深度学习。他们从应用、技术和挑战的角度探讨了各种方法和模型。
Deng(2011)从信息处理及相关领域的角度对深度结构化学习及其架构进行了概述。
Arel等人(2010)简要概述了近年来的DL技术。
Bengio(2009)讨论了深度架构,即人工智能的神经网络和生成模型。
最近所有关于深度学习(DL)的论文都从多个角度讨论了深度学习重点。这对DL的研究人员来说是非常有必要的。然而,DL目前是一个蓬勃发展的领域。在最近的DL概述论文发表之后,仍有许多新的技术和架构被提出。此外,以往的论文从不同的角度进行研究。我们的论文主要是针对刚进入这一领域的学习者和新手。为此,我们将努力为新研究人员和任何对这一领域感兴趣的人提供一个深度学习的基础和清晰的概念。


Recent Advances

在本节中,我们将讨论最近从机器学习和人工神经网络(ANN)的中衍生出来的主要深度学习(DL)方法,人工神经网络是深度学习最常用的形式。

Evolution of Deep Architectures

人工神经网络(ANN)已经取得了长足的进步,同时也带来了其他的深度模型。第一代人工神经网络由简单的感知器神经层组成,只能进行有限的简单计算。第二代使用反向传播,根据错误率更新神经元的权重。然后支持向量机(SVM)浮出水面,在一段时间内超越 ANN。为了克服反向传播的局限性,人们提出了受限玻尔兹曼机(RBM),使学习更容易。此时其他技术和神经网络也出现了,如前馈神经网络(FNN)、卷积神经网络(CNN)、循环神经网络(RNN)等,以及深层信念网络、自编码器等。从那时起,为实现各种用途,ANN在不同方面得到了改进和设计。
Schmidhuber(2014)、Bengio(2009)、Deng和Yu(2014)、Goodfellow等人(2016)、Wang等人(2017)对深度神经网络(DNN)的进化和历史以及深度学习(DL)进行了详细的概述。在大多数情况下,深层架构是简单架构的多层非线性重复,这样可从输入中获得高度复杂的函数。


Deep Learning Approaches

深度神经网络在监督学习中取得了巨大的成功。此外,深度学习模型在无监督、混合和强化学习方面也非常成功。

Deep Supervised Learning

监督学习应用在当数据标记、分类器分类或数值预测的情况。LeCun等人(2015)对监督学习方法以及深层结构的形成给出了一个精简的解释。Deng和Yu(2014)提到了许多用于监督和混合学习的深度网络,并做出解释,例如深度堆栈网络(DSN)及其变体。Schmidthuber(2014)的研究涵盖了所有神经网络,从早期神经网络到最近成功的卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆(LSTM)及其改进。

Deep Unsupervised Learning

当输入数据没有标记时,可应用无监督学习方法从数据中提取特征并对其进行分类或标记。LeCun等人(2015)预测了无监督学习在深度学习中的未来。Schmidthuber(2014)也描述了无监督学习的神经网络。Deng和Yu(2014)简要介绍了无监督学习的深度架构,并详细解释了深度自编码器。

Deep Reinforcement Learning

强化学习使用奖惩系统预测学习模型的下一步。这主要用于游戏和机器人,解决平常的决策问题。Schmidthuber(2014)描述了强化学习(RL)中深度学习的进展,以及深度前馈神经网络(FNN)和循环神经网络(RNN)在RL中的应用。Li(2017)讨论了深度强化学习(Deep Reinforcement Learning,DRL)、它的架构(例如Deep Q-Network,DQN)以及在各个领域的应用。
Mnih等人(2016)提出了一种利用异步梯度下降进行DNN优化的DRL框架。
van Hasselt等人(2015)提出了一种使用深度神经网络(deep neural network,DNN)的DRL架构。


Deep Neural Networks

在本节中,我们将简要地讨论深度神经网络(DNN),以及它们最近的改进和突破。神经网络的功能与人脑相似。它们主要由神经元和连接组成。当我们说深度神经网络时,我们可以假设有相当多的隐藏层,可以用来从输入中提取特征和计算复杂的函数。Bengio(2009)解释了深度结构的神经网络,如卷积神经网络(CNN)、自编码器(AE)等及其变体。Deng和Yu(2014)详细介绍了一些神经网络架构,如AE及其变体。Goodfellow等(2016)对深度前馈网络、卷积网络、递归网络及其改进进行了介绍和技巧性讲解。Schmidhuber(2014)提到了神经网络从早期神经网络到最近成功技术的完整历史。

Deep Autoencoders

自编码器(AE)是神经网络(NN),其中输出即输入。AE采用原始输入,编码为压缩表示,然后解码以重建输入。在深度AE中,低隐藏层用于编码,高隐藏层用于解码,误差反向传播用于训练。

Variational Autoencoders

变分自动编码器(VAE)可以算作解码器。VAE建立在标准神经网络上,可以通过随机梯度下降训练(Doersch,2016)。

Stacked Denoising Autoencoders

在早期的自编码器(AE)中,编码层的维度比输入层小(窄)。在多层降噪自编码器(SDAE)中,编码层比输入层宽(Deng and Yu,2014)。

Transforming Autoencoders

深度自动编码器(DAE)可以是转换可变的,也就是从多层非线性处理中提取的特征可以根据学习者的需要而改变。变换自编码器(TAE)既可以使用输入向量,也可以使用目标输出向量来应用转换不变性属性,将代码引导到期望的方向(Deng and Yu,2014)。

Deep Convolutional Neural Networks

四种基本思想构成了卷积神经网络(CNN),即:局部连接、共享权重、池化和多层使用。CNN的第一部分由卷积层和池化层组成,后一部分主要是全连接层。卷积层检测特征的局部连接,池层将相似的特征合并为一个。CNN在卷积层中使用卷积而不是矩阵乘法。
Krizhevsky等人(2012)提出了一种深度卷积神经网络(CNN)架构,也称为AlexNet,这是深度学习(Deep Learning,DL)的一个重大突破。网络由5个卷积层和3个全连接层组成。该架构采用图形处理单元(GPU)进行卷积运算,采用线性整流函数(ReLU)作为激活函数,用Dropout来减少过拟合。
Iandola等人(2016)提出了一个小型的CNN架构,叫做SqueezeNet。
Szegedy等人(2014)提出了一种深度CNN架构,名为Inception。Dai等人(2017)提出了对Inception-ResNet的改进。
Redmon等人(2015)提出了一个名为YOLO(You Only Look Once)的CNN架构,用于均匀和实时的目标检测。
Zeiler和Fergus(2013)提出了一种将CNN内部激活可视化的方法。
Gehring等人(2017)提出了一种用于序列到序列学习的CNN架构。
Bansal等人(2017)提出了PixelNet,使用像素来表示。
Goodfellow等人(2016)解释了CNN的基本架构和思想。Gu等人(2015)对CNN的最新进展、CNN的多种变体、CNN的架构、正则化方法和功能以及在各个领域的应用进行了很好的概述。

Deep Max-Pooling Convolutional Neural Networks

最大池化卷积神经网络(MPCNN)主要对卷积和最大池化进行操作,特别是在数字图像处理中。MPCNN通常由输入层以外的三种层组成。卷积层获取输入图像并生成特征图,然后应用非线性激活函数。最大池层向下采样图像,并保持子区域的最大值。全连接层进行线性乘法。在深度MPCNN中,在输入层之后周期性地使用卷积和混合池化,然后是全连接层。

Very Deep Convolutional Neural Networks

Simonyan和Zisserman(2014)提出了非常深层的卷积神经网络(VDCNN)架构,也称为VGG Net。VGG Net使用非常小的卷积滤波器,深度达到 16-19层。Conneau等人(2016)提出了另一种文本分类的VDCNN架构,使用小卷积和池化。他们声称这个VDCNN架构是第一个在文本处理中使用的,它在字符级别上起作用。该架构由29个卷积层组成。

Network In Network

Lin等人(2013)提出了网络中的网络(Network In Network,NIN)。NIN以具有复杂结构的微神经网络代替传统卷积神经网络(CNN)的卷积层。它使用多层感知器(MLPConv)处理微神经网络和全局平均池化层,而不是全连接层。深度NIN架构可以由NIN结构的多重叠加组成。

Region-based Convolutional Neural Networks

Girshick等人(2014)提出了基于区域的卷积神经网络(R-CNN),使用区域进行识别。R-CNN使用区域来定位和分割目标。该架构由三个模块组成:定义了候选区域的集合的类别独立区域建议,从区域中提取特征的大型卷积神经网络(CNN),以及一组类特定的线性支持向量机(SVM)。

Fast R-CNN

Girshick(2015)提出了快速的基于区域的卷积网络(Fast R-CNN)。这种方法利用R-CNN架构能快速地生成结果。Fast R-CNN由卷积层和池化层、区域建议层和一系列全连接层组成。

Faster R-CNN

Ren等人(2015)提出了更快的基于区域的卷积神经网络(Faster R-CNN),它使用区域建议网络(Region Proposal Network,RPN)进行实时目标检测。RPN是一个全卷积网络,能够准确、高效地生成区域建议(Ren et al.,2015)。

Mask R-CNN

何恺明等人(2017)提出了基于区域的掩模卷积网络(Mask R-CNN)实例目标分割。Mask R-CNN扩展了R-CNN的架构,并使用一个额外的分支用于预测目标掩模。

Multi-Expert R-CNN

Lee等人(2017)提出了基于区域的多专家卷积神经网络(ME R-CNN),利用了Fast R-CNN架构。ME R-CNN从选择性和详尽的搜索中生成兴趣区域(RoI)。它也使用per-RoI多专家网络而不是单一的per-RoI网络。每个专家都是来自Fast R-CNN的全连接层的相同架构。

Deep Residual Networks

He等人(2015)提出的残差网络(ResNet)由152层组成。ResNet具有较低的误差,并且容易通过残差学习进行训练。更深层次的ResNet可以获得更好的性能。在深度学习领域,人们认为ResNet是一个重要的进步。

Resnet in Resnet

Targ等人(2016)在Resnet in Resnet(RiR)中提出将ResNets和标准卷积神经网络(CNN)结合到深层双流架构中。

ResNeXt

Xie等人(2016)提出了ResNeXt架构。ResNext利用ResNets来重复使用分割-转换-合并策略。

Capsule Networks

Sabour等人(2017)提出了胶囊网络(CapsNet),即一个包含两个卷积层和一个全连接层的架构。CapsNet通常包含多个卷积层,胶囊层位于末端。CapsNet被认为是深度学习的最新突破之一,因为据说这是基于卷积神经网络的局限性而提出的。它使用的是一层又一层的胶囊,而不是神经元。激活的较低级胶囊做出预测,在同意多个预测后,更高级的胶囊变得活跃。在这些胶囊层中使用了一种协议路由机制。Hinton之后提出EM路由,利用期望最大化(EM)算法对CapsNet进行了改进。

Recurrent Neural Networks

循环神经网络(RNN)更适合于序列输入,如语音、文本和生成序列。一个重复的隐藏单元在时间展开时可以被认为是具有相同权重的非常深的前馈网络。由于梯度消失和维度爆炸问题,RNN曾经很难训练。为了解决这个问题,后来许多人提出了改进意见。
Goodfellow等人(2016)详细分析了循环和递归神经网络和架构的细节,以及相关的门控和记忆网络。
Karpathy等人(2015)使用字符级语言模型来分析和可视化预测、表征训练动态、RNN及其变体(如LSTM)的错误类型等。
J´ozefowicz等人(2016)探讨了RNN模型和语言模型的局限性。

RNN-EM

Peng和Yao(2015)提出了利用外部记忆(RNN-EM)来改善RNN的记忆能力。他们声称在语言理解方面达到了最先进的水平,比其他RNN更好。

GF-RNN

Chung等人(2015)提出了门控反馈递归神经网络(GF-RNN),它通过将多个递归层与全局门控单元叠加来扩展标准的RNN。

CRF-RNN

Zheng等人(2015)提出条件随机场作为循环神经网络(CRF-RNN),其将卷积神经网络(CNN)和条件随机场(CRF)结合起来进行概率图形建模。

Quasi-RNN

Bradbury等人(2016)提出了用于神经序列建模和沿时间步的并行应用的准循环神经网络(QRNN)。

Memory Networks

Weston等人(2014)提出了问答记忆网络(QA)。记忆网络由记忆、输入特征映射、泛化、输出特征映射和响应组成。

Dynamic Memory Networks

Kumar等人(2015)提出了用于QA任务的动态记忆网络(DMN)。DMN有四个模块:输入、问题、情景记忆、输出。

Augmented Neural Networks

Olah和Carter(2016)很好地展示了注意力和增强循环神经网络,即神经图灵机(NTM)、注意力接口、神经编码器和自适应计算时间。增强神经网络通常是使用额外的属性,如逻辑函数以及标准的神经网络架构。

Neural Turing Machines

Graves等人(2014)提出了神经图灵机(NTM)架构,由神经网络控制器和记忆库组成。NTM通常将RNN与外部记忆库结合。

Neural GPU

Kaiser和Sutskever(2015)提出了神经 GPU,解决了NTM的并行问题。

Neural Random-Access Machines

Kurach等人(2015)提出了神经随机存取机,它使用外部的可变大小的随机存取存储器。

Neural Programmer

Neelakantan等人(2015)提出了神经编程器,一种具有算术和逻辑功能的增强神经网络。

Neural Programmer-Interpreters

Reed和de Freitas(2015)提出了可以学习的神经编程器-解释器(NPI)。NPI包括周期性内核、程序内存和特定于领域的编码器。

Long Short Term Memory Networks

Hochreiter和Schmidhuber(1997)提出了长短期记忆(Long Short-Term Memory,LSTM),克服了循环神经网络(RNN)的误差回流问题。LSTM是基于循环网络和基于梯度的学习算法,LSTM引入自循环产生路径,使得梯度能够流动。
Greff等人(2017)对标准LSTM和8个LSTM变体进行了大规模分析,分别用于语音识别、手写识别和复调音乐建模。他们声称LSTM的8个变体没有显著改善,而只有标准LSTM表现良好。
Shi等人(2016)提出了深度长短期记忆网络(DLSTM),它是一个LSTM单元的堆栈,用于特征映射学习表示。

Batch-Normalized LSTM

Cooijmans等人(2016)提出了批归一化LSTM(BN-LSTM),它对递归神经网络的隐藏状态使用批归一化。

Pixel RNN

van den Oord等人(2016)提出像素递归神经网络(Pixel-RNN),由12个二维LSTM层组成。

Bidirectional LSTM

W¨ollmer等人(2010)提出了双向LSTM(BLSTM)的循环网络与动态贝叶斯网络(DBN)一起用于上下文敏感关键字检测。

Variational Bi-LSTM

Shabanian等人(2017)提出了变分双向LSTM(Variational Bi-LSTM),它是双向LSTM体系结构的变体。Variational Bi-LSTM使用变分自编码器(VAE)在LSTM之间创建一个信息交换通道,以学习更好的表征。

Googles Neural Machine Translation

Wu等人(2016)提出了名为谷歌神经机器翻译(GNMT)的自动翻译系统,该系统结合了编码器网络、解码器网络和注意力网络,遵循共同的序列对序列(sequence-to-sequence)的学习框架。

Fader Network

Lample等人(2017)提出了Fader网络,这是一种新型的编码器-解码器架构,通过改变属性值来生成真实的输入图像变化。

Hyper Networks

Ha等人(2016)提出的超网络(Hyper Networks)为其他神经网络生成权值,如静态超网络卷积网络、用于循环网络的动态超网络。
Deutsch(2018)使用超网络生成神经网络。

Highway Networks

Srivastava等人(2015)提出了高速路网络(Highway Networks),通过使用门控单元来学习管理信息。跨多个层次的信息流称为信息高速路。

Recurrent Highway Networks

Zilly等人(2017)提出了循环高速路网络(Recurrent Highway Networks,RHN),它扩展了长短期记忆(LSTM)架构。RHN在周期性过渡中使用了Highway层。

Highway LSTM RNN

Zhang等人(2016)提出了高速路长短期记忆Highway Long Short-Term Memory(HLSTM)RNN,它在相邻层的内存单元之间扩展了具有封闭方向连接(即Highway)的深度LSTM网络。

Long-Term Recurrent CNN

Donahue等人(2014)提出了长期循环卷积网络(LRCN),它使用CNN进行输入,然后使用LSTM进行递归序列建模并生成预测。

Deep Neural SVM

Zhang等人(2015)提出了深度神经SVM(DNSVM),它以支持向量机(Support Vector Machine,SVM)作为深度神经网络(Deep Neural Network,DNN)分类的顶层。

Convolutional Residual Memory Networks

Moniz和Pal(2016)提出了卷积残差记忆网络,将记忆机制并入卷积神经网络(CNN)。它用一个长短期记忆机制来增强卷积残差网络。

Fractal Networks

Larsson等人(2016)提出分形网络即FractalNet作为残差网络的替代方案。他们声称可以训练超深度的神经网络而不需要残差学习。分形是简单扩展规则生成的重复架构。

WaveNet

van den Oord等人(2016)提出了用于产生原始音频的深度神经网络WaveNet。WaveNet由一堆卷积层和softmax分布层组成,用于输出。
Rethage等人(2017)提出了一个WaveNet模型用于语音去噪。

Pointer Networks

Vinyals等人(2017)提出了指针网络(Ptr-Nets),通过使用一种称为“指针”的softmax概率分布来解决表征变量字典的问题。


Deep Generative Models

在本节中,我们将简要讨论其他深度架构,它们使用与深度神经网络类似的多个抽象层和表示层,也称为深度生成模型(deep generate Models,DGM)。Bengio(2009)解释了深层架构,例如Boltzmann machines(BM)和Restricted Boltzmann Machines(RBM)等及其变体。
Goodfellow等人(2016)详细解释了深度生成模型,如受限和非受限的玻尔兹曼机及其变种、深度玻尔兹曼机、深度信念网络(DBN)、定向生成网络和生成随机网络等。
Maaløe等人(2016)提出了辅助的深层生成模型(Auxiliary Deep Generative Models),在这些模型中,他们扩展了具有辅助变量的深层生成模型。辅助变量利用随机层和跳过连接生成变分分布。
Rezende等人(2016)开发了一种深度生成模型的单次泛化。

Boltzmann Machines

玻尔兹曼机是学习任意概率分布的连接主义方法,使用最大似然原则进行学习。

Restricted Boltzmann Machines

受限玻尔兹曼机(Restricted Boltzmann Machines,RBM)是马尔可夫随机场的一种特殊类型,包含一层随机隐藏单元,即潜变量和一层可观测变量。
Hinton和Salakhutdinov(2011)提出了一种利用受限玻尔兹曼机(RBM)进行文档处理的深度生成模型。

Deep Belief Networks

深度信念网络(Deep Belief Networks,DBN)是具有多个潜在二元或真实变量层的生成模型。
Ranzato等人(2011)利用深度信念网络(deep Belief Network,DBN)建立了深度生成模型进行图像识别。

Deep Lambertian Networks

Tang等人(2012)提出了深度朗伯网络(Deep Lambertian Networks,DLN),它是一个多层次的生成模型,其中潜在的变量是反照率、表面法线和光源。DLNis是朗伯反射率与高斯受限玻尔兹曼机和深度信念网络的结合。

Generative Adversarial Networks

Goodfellow等人(2014)提出了生成对抗网络(Generate Adversarial Nets,GAN),用于通过对抗过程来评估生成模型。GAN架构是由一个针对对手(即一个学习模型或数据分布的判别模型)的生成模型组成。Mao等人(2016)、Kim等人(2017)对GAN提出了更多的改进。
Salimans等人(2016)提出了几种训练GANs的方法。

Laplacian Generative Adversarial Networks

Denton等人(2015)提出了一种深度生成模型(DGM),叫做拉普拉斯生成对抗网络(LAPGAN),使用生成对抗网络(GAN)方法。该模型还在拉普拉斯金字塔框架中使用卷积网络。

Recurrent Support Vector Machines

Shi等人(2016)提出了循环支持向量机(RSVM),利用循环神经网络(RNN)从输入序列中提取特征,用标准支持向量机(SVM)进行序列级目标识别。


Training and Optimization Techniques

在本节中,我们将简要概述一些主要的技术,用于正则化和优化深度神经网络(DNN)。

Dropout

Srivastava等人(2014)提出Dropout,以防止神经网络过拟合。Dropout是一种神经网络模型平均正则化方法,通过增加噪声到其隐藏单元。在训练过程中,它会从神经网络中随机抽取出单元和连接。Dropout可以用于像RBM(Srivastava et al.,2014)这样的图形模型中,也可以用于任何类型的神经网络。最近提出的一个关于Dropout的改进是Fraternal Dropout,用于循环神经网络(RNN)。

Maxout

Goodfellow等人(2013)提出Maxout,一种新的激活函数,用于Dropout。Maxout的输出是一组输入的最大值,有利于Dropout的模型平均。

Zoneout

Krueger等人(2016)提出了循环神经网络(RNN)的正则化方法Zoneout。Zoneout在训练中随机使用噪音,类似于Dropout,但保留了隐藏的单元而不是丢弃。

Deep Residual Learning

He等人(2015)提出了深度残差学习框架,该框架被称为低训练误差的ResNet。

Batch Normalization

Ioffe和Szegedy(2015)提出了批归一化,通过减少内部协变量移位来加速深度神经网络训练的方法。Ioffe(2017)提出批重归一化,扩展了以前的方法。

Distillation

Hinton等人(2015)提出了将知识从高度正则化模型的集合(即神经网络)转化为压缩小模型的方法。

Layer Normalization

Ba等人(2016)提出了层归一化,特别是针对RNN的深度神经网络加速训练,解决了批归一化的局限性。


Deep Learning frameworks

有大量的开源库和框架可供深度学习使用。它们大多数是为Python编程语言构建的。如Theano、Tensorflow、PyTorch、PyBrain、Caffe、Blocks and Fuel、CuDNN、Honk、ChainerCV、PyLearn2、Chainer、torch等。


Applications of Deep Learning

在本节中,我们将简要地讨论一些最近在深度学习方面的杰出应用。自深度学习(DL)开始以来,DL方法以监督、非监督、半监督或强化学习的形式被广泛应用于各个领域。从分类和检测任务开始,DL应用正在迅速扩展到每一个领域。
例如:

等。
Deng和Yu(2014)提供了DL在语音处理、信息检索、目标识别、计算机视觉、多模态、多任务学习等领域应用的详细列表。
使用深度强化学习(Deep Reinforcement Learning,DRL)来掌握游戏已经成为当今的一个热门话题。每到现在,人工智能机器人都是用DNN和DRL创建的,它们在战略和其他游戏中击败了人类世界冠军和象棋大师,从几个小时的训练开始。例如围棋的AlphaGo和AlphaGo Zero。


Discussion

尽管深度学习在许多领域取得了巨大的成功,但它还有很长的路要走。还有很多地方有待改进。至于局限性,例子也是相当多的。例如:Nguyen等人表明深度神经网络(DNN)在识别图像时容易被欺骗。还有其他问题,如Yosinski等人提出的学习的特征可迁移性。Huang等人提出了一种神经网络攻击防御的体系结构,认为未来的工作需要防御这些攻击。Zhang等人则提出了一个理解深度学习模型的实验框架,他们认为理解深度学习需要重新思考和概括。
Marcus在2018年对深度学习(Deep Learning,DL)的作用、局限性和本质进行了重要的回顾。他强烈指出了DL方法的局限性,即需要更多的数据,容量有限,不能处理层次结构,无法进行开放式推理,不能充分透明,不能与先验知识集成,不能区分因果关系。他还提到,DL假设了一个稳定的世界,以近似方法实现,工程化很困难,并且存在着过度炒作的潜在风险。Marcus认为DL需要重新概念化,并在非监督学习、符号操作和混合模型中寻找可能性,从认知科学和心理学中获得见解,并迎接更大胆的挑战。


Conclusion

尽管深度学习(DL)比以往任何时候都更快地推进了世界的发展,但仍有许多方面值得我们去研究。我们仍然无法完全地理解深度学习,我们如何让机器变得更聪明,更接近或比人类更聪明,或者像人类一样学习。DL一直在解决许多问题,同时将技术应用到方方面面。但是人类仍然面临着许多难题,例如仍有人死于饥饿和粮食危机,癌症和其他致命的疾病等。我们希望深度学习和人工智能将更加致力于改善人类的生活质量,通过开展最困难的科学研究。最后但也是最重要的,愿我们的世界变得更加美好。

]]>
+

今天在公众号上看到一篇综述论文的翻译,该论文列举出了近年来深度学习的一些重要研究成果,从方法、架构,以及正则化、优化技术方面进行概述。看完之后感觉对目前学术图景有了一次基本的认识,于是决定搬过来,对之后参考文献查找也能起到很大的帮助。
之前也分享过一篇deep-learning笔记:一篇非常经典的论文——NatureDeepReview,可以一起看一下。
觉得阅读英文版更好,或者看完后想寻找对应参考文献的读者可以直接去论文地址下载原文。


Abstract

深度学习是机器学习和人工智能研究的最新趋势之一。它也是当今最流行的科学研究趋势之一。深度学习方法为计算机视觉和机器学习带来了革命性的进步。新的深度学习技术正在不断诞生,超越最先进的机器学习甚至是现有的深度学习技术。近年来,全世界在这一领域取得了许多重大突破。由于深度学习正快度发展,导致了它的进展很难被跟进,特别是对于新的研究者。在本文中,我们将简要讨论近年来关于深度学习的最新进展。


Introduction

“深度学习”(DL)一词最初在1986年被引入机器学习(ML),后来在2000年时被用于人工神经网络(ANN)。深度学习方法由多个层组成,以学习具有多个抽象层次的数据特征。DL方法允许计算机通过相对简单的概念来学习复杂的概念。对于人工神经网络(ANN),深度学习(DL)(也称为分层学习(Hierarchical Learning))是指在多个计算阶段中精确地分配信用,以转换网络中的聚合激活。为了学习复杂的功能,深度架构被用于多个抽象层次,即非线性操作;例如 ANNs,具有许多隐藏层。用准确的话总结就是,深度学习是机器学习的一个子领域,它使用了多层次的非线性信息处理和抽象,用于有监督或无监督的特征学习、表示、分类和模式识别。
深度学习即表征学习是机器学习的一个分支或子领域,大多数人认为近代深度学习方法是从2006年开始发展起来的。本文是关于最新的深度学习技术的综述,主要推荐给即将涉足该领域的研究者。本文包括DL的基本思想、主要方法、最新进展以及应用。
综述论文是非常有益的,特别是对某一特定领域的新研究人员。一个研究领域如果在不久的将来及相关应用领域中有很大的价值,那通常很难被实时跟踪到最新进展。现在,科学研究是一个很有吸引力的职业,因为知识和教育比以往任何时候都更容易分享和获得。对于一种技术研究的趋势来说,唯一正常的假设是它会在各个方面有很多的改进。几年前对某个领域的概述,现在可能已经过时了。
考虑到近年来深度学习的普及和推广,我们简要概述了深度学习和神经网络(NN),以及它的主要进展和几年来的重大突破。我们希望这篇文章将帮助许多新手研究者在这一领域全面了解最近的深度学习的研究和技术,并引导他们以正确的方式开始。同时,我们希望通过这项工作,向这个时代的顶级DL和ANN研究者们致敬:Geoffrey Hinton(Hinton)、Juergen Schmidhuber(Schmidhuber)、Yann LeCun(LeCun)、Yoshua Bengio(Bengio)和许多其他研究学者,他们的研究构建了现代人工智能(AI)。跟进他们的工作,以追踪当前最佳的DL和ML研究进展对我们来说也至关重要。
在本论文中,我们首先简述过去的研究论文,对深度学习的模型和方法进行研究。然后,我们将开始描述这一领域的最新进展。我们将讨论深度学习(DL)方法、深度架构(即深度神经网络(DNN))和深度生成模型(DGM),其次是重要的正则化和优化方法。此外,用两个简短的部分对于开源的 DL 框架和重要的 DL 应用进行总结。我们将在最后两个章节(即讨论和结论)中讨论深入学习的现状和未来。


Related works

在过去的几年中,有许多关于深度学习的综述论文。他们以很好的方式描述了DL方法、方法论以及它们的应用和未来研究方向。这里,我们简要介绍一些关于深度学习的优秀综述论文。
Young等人(2017)讨论了DL模型和架构,主要用于自然语言处理(NLP)。他们在不同的NLP领域中展示了DL应用,比较了DL模型,并讨论了可能的未来趋势。
Zhang等人(2017)讨论了用于前端和后端语音识别系统的当前最佳深度学习技术。
Zhu等人(2017)综述了DL遥感技术的最新进展。他们还讨论了开源的DL框架和其他深度学习的技术细节。
Wang等人(2017)以时间顺序的方式描述了深度学习模型的演变。该短文简要介绍了模型,以及在DL研究中的突破。该文以进化的方式来了解深度学习的起源,并对神经网络的优化和未来的研究做了解读。
Goodfellow等人(2016)详细讨论了深度网络和生成模型,从机器学习(ML)基础知识、深度架构的优缺点出发,对近年来的DL研究和应用进行了总结。
LeCun等人(2015)从卷积神经网络(CNN)和递归神经网络(RNN)概述了深度学习(DL)模型。他们从表征学习的角度描述了DL,展示了DL技术如何工作、如何在各种应用中成功使用、以及如何对预测未来进行基于无监督学习(UL)的学习。同时他们还指出了DL在文献目录中的主要进展。
Schmidhuber(2015)从CNN、RNN和深度强化学习(RL)对深度学习做了一个概述。他强调了序列处理的RNN,同时指出基本DL和NN的局限性,以及改进它们的技巧。
Nielsen(2015)用代码和例子描述了神经网络的细节。他还在一定程度上讨论了深度神经网络和深度学习。
Schmidhuber(2014)讨论了基于时间序列的神经网络、采用机器学习方法进行分类,以及在神经网络中使用深度学习的历史和进展。
Deng和Yu(2014)描述了深度学习类别和技术,以及DL在几个领域的应用。
Bengio(2013)从表征学习的角度简要概述了DL算法,即监督和无监督网络、优化和训练模型。他聚焦于深度学习的许多挑战,例如:为更大的模型和数据扩展算法,减少优化困难,设计有效的缩放方法等。
Bengio等人(2013)讨论了表征和特征学习即深度学习。他们从应用、技术和挑战的角度探讨了各种方法和模型。
Deng(2011)从信息处理及相关领域的角度对深度结构化学习及其架构进行了概述。
Arel等人(2010)简要概述了近年来的DL技术。
Bengio(2009)讨论了深度架构,即人工智能的神经网络和生成模型。
最近所有关于深度学习(DL)的论文都从多个角度讨论了深度学习重点。这对DL的研究人员来说是非常有必要的。然而,DL目前是一个蓬勃发展的领域。在最近的DL概述论文发表之后,仍有许多新的技术和架构被提出。此外,以往的论文从不同的角度进行研究。我们的论文主要是针对刚进入这一领域的学习者和新手。为此,我们将努力为新研究人员和任何对这一领域感兴趣的人提供一个深度学习的基础和清晰的概念。


Recent Advances

在本节中,我们将讨论最近从机器学习和人工神经网络(ANN)的中衍生出来的主要深度学习(DL)方法,人工神经网络是深度学习最常用的形式。

Evolution of Deep Architectures

人工神经网络(ANN)已经取得了长足的进步,同时也带来了其他的深度模型。第一代人工神经网络由简单的感知器神经层组成,只能进行有限的简单计算。第二代使用反向传播,根据错误率更新神经元的权重。然后支持向量机(SVM)浮出水面,在一段时间内超越 ANN。为了克服反向传播的局限性,人们提出了受限玻尔兹曼机(RBM),使学习更容易。此时其他技术和神经网络也出现了,如前馈神经网络(FNN)、卷积神经网络(CNN)、循环神经网络(RNN)等,以及深层信念网络、自编码器等。从那时起,为实现各种用途,ANN在不同方面得到了改进和设计。
Schmidhuber(2014)、Bengio(2009)、Deng和Yu(2014)、Goodfellow等人(2016)、Wang等人(2017)对深度神经网络(DNN)的进化和历史以及深度学习(DL)进行了详细的概述。在大多数情况下,深层架构是简单架构的多层非线性重复,这样可从输入中获得高度复杂的函数。


Deep Learning Approaches

深度神经网络在监督学习中取得了巨大的成功。此外,深度学习模型在无监督、混合和强化学习方面也非常成功。

Deep Supervised Learning

监督学习应用在当数据标记、分类器分类或数值预测的情况。LeCun等人(2015)对监督学习方法以及深层结构的形成给出了一个精简的解释。Deng和Yu(2014)提到了许多用于监督和混合学习的深度网络,并做出解释,例如深度堆栈网络(DSN)及其变体。Schmidthuber(2014)的研究涵盖了所有神经网络,从早期神经网络到最近成功的卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆(LSTM)及其改进。

Deep Unsupervised Learning

当输入数据没有标记时,可应用无监督学习方法从数据中提取特征并对其进行分类或标记。LeCun等人(2015)预测了无监督学习在深度学习中的未来。Schmidthuber(2014)也描述了无监督学习的神经网络。Deng和Yu(2014)简要介绍了无监督学习的深度架构,并详细解释了深度自编码器。

Deep Reinforcement Learning

强化学习使用奖惩系统预测学习模型的下一步。这主要用于游戏和机器人,解决平常的决策问题。Schmidthuber(2014)描述了强化学习(RL)中深度学习的进展,以及深度前馈神经网络(FNN)和循环神经网络(RNN)在RL中的应用。Li(2017)讨论了深度强化学习(Deep Reinforcement Learning,DRL)、它的架构(例如Deep Q-Network,DQN)以及在各个领域的应用。
Mnih等人(2016)提出了一种利用异步梯度下降进行DNN优化的DRL框架。
van Hasselt等人(2015)提出了一种使用深度神经网络(deep neural network,DNN)的DRL架构。


Deep Neural Networks

在本节中,我们将简要地讨论深度神经网络(DNN),以及它们最近的改进和突破。神经网络的功能与人脑相似。它们主要由神经元和连接组成。当我们说深度神经网络时,我们可以假设有相当多的隐藏层,可以用来从输入中提取特征和计算复杂的函数。Bengio(2009)解释了深度结构的神经网络,如卷积神经网络(CNN)、自编码器(AE)等及其变体。Deng和Yu(2014)详细介绍了一些神经网络架构,如AE及其变体。Goodfellow等(2016)对深度前馈网络、卷积网络、递归网络及其改进进行了介绍和技巧性讲解。Schmidhuber(2014)提到了神经网络从早期神经网络到最近成功技术的完整历史。

Deep Autoencoders

自编码器(AE)是神经网络(NN),其中输出即输入。AE采用原始输入,编码为压缩表示,然后解码以重建输入。在深度AE中,低隐藏层用于编码,高隐藏层用于解码,误差反向传播用于训练。

Variational Autoencoders

变分自动编码器(VAE)可以算作解码器。VAE建立在标准神经网络上,可以通过随机梯度下降训练(Doersch,2016)。

Stacked Denoising Autoencoders

在早期的自编码器(AE)中,编码层的维度比输入层小(窄)。在多层降噪自编码器(SDAE)中,编码层比输入层宽(Deng and Yu,2014)。

Transforming Autoencoders

深度自动编码器(DAE)可以是转换可变的,也就是从多层非线性处理中提取的特征可以根据学习者的需要而改变。变换自编码器(TAE)既可以使用输入向量,也可以使用目标输出向量来应用转换不变性属性,将代码引导到期望的方向(Deng and Yu,2014)。

Deep Convolutional Neural Networks

四种基本思想构成了卷积神经网络(CNN),即:局部连接、共享权重、池化和多层使用。CNN的第一部分由卷积层和池化层组成,后一部分主要是全连接层。卷积层检测特征的局部连接,池层将相似的特征合并为一个。CNN在卷积层中使用卷积而不是矩阵乘法。
Krizhevsky等人(2012)提出了一种深度卷积神经网络(CNN)架构,也称为AlexNet,这是深度学习(Deep Learning,DL)的一个重大突破。网络由5个卷积层和3个全连接层组成。该架构采用图形处理单元(GPU)进行卷积运算,采用线性整流函数(ReLU)作为激活函数,用Dropout来减少过拟合。
Iandola等人(2016)提出了一个小型的CNN架构,叫做SqueezeNet。
Szegedy等人(2014)提出了一种深度CNN架构,名为Inception。Dai等人(2017)提出了对Inception-ResNet的改进。
Redmon等人(2015)提出了一个名为YOLO(You Only Look Once)的CNN架构,用于均匀和实时的目标检测。
Zeiler和Fergus(2013)提出了一种将CNN内部激活可视化的方法。
Gehring等人(2017)提出了一种用于序列到序列学习的CNN架构。
Bansal等人(2017)提出了PixelNet,使用像素来表示。
Goodfellow等人(2016)解释了CNN的基本架构和思想。Gu等人(2015)对CNN的最新进展、CNN的多种变体、CNN的架构、正则化方法和功能以及在各个领域的应用进行了很好的概述。

Deep Max-Pooling Convolutional Neural Networks

最大池化卷积神经网络(MPCNN)主要对卷积和最大池化进行操作,特别是在数字图像处理中。MPCNN通常由输入层以外的三种层组成。卷积层获取输入图像并生成特征图,然后应用非线性激活函数。最大池层向下采样图像,并保持子区域的最大值。全连接层进行线性乘法。在深度MPCNN中,在输入层之后周期性地使用卷积和混合池化,然后是全连接层。

Very Deep Convolutional Neural Networks

Simonyan和Zisserman(2014)提出了非常深层的卷积神经网络(VDCNN)架构,也称为VGG Net。VGG Net使用非常小的卷积滤波器,深度达到 16-19层。Conneau等人(2016)提出了另一种文本分类的VDCNN架构,使用小卷积和池化。他们声称这个VDCNN架构是第一个在文本处理中使用的,它在字符级别上起作用。该架构由29个卷积层组成。

Network In Network

Lin等人(2013)提出了网络中的网络(Network In Network,NIN)。NIN以具有复杂结构的微神经网络代替传统卷积神经网络(CNN)的卷积层。它使用多层感知器(MLPConv)处理微神经网络和全局平均池化层,而不是全连接层。深度NIN架构可以由NIN结构的多重叠加组成。

Region-based Convolutional Neural Networks

Girshick等人(2014)提出了基于区域的卷积神经网络(R-CNN),使用区域进行识别。R-CNN使用区域来定位和分割目标。该架构由三个模块组成:定义了候选区域的集合的类别独立区域建议,从区域中提取特征的大型卷积神经网络(CNN),以及一组类特定的线性支持向量机(SVM)。

Fast R-CNN

Girshick(2015)提出了快速的基于区域的卷积网络(Fast R-CNN)。这种方法利用R-CNN架构能快速地生成结果。Fast R-CNN由卷积层和池化层、区域建议层和一系列全连接层组成。

Faster R-CNN

Ren等人(2015)提出了更快的基于区域的卷积神经网络(Faster R-CNN),它使用区域建议网络(Region Proposal Network,RPN)进行实时目标检测。RPN是一个全卷积网络,能够准确、高效地生成区域建议(Ren et al.,2015)。

Mask R-CNN

何恺明等人(2017)提出了基于区域的掩模卷积网络(Mask R-CNN)实例目标分割。Mask R-CNN扩展了R-CNN的架构,并使用一个额外的分支用于预测目标掩模。

Multi-Expert R-CNN

Lee等人(2017)提出了基于区域的多专家卷积神经网络(ME R-CNN),利用了Fast R-CNN架构。ME R-CNN从选择性和详尽的搜索中生成兴趣区域(RoI)。它也使用per-RoI多专家网络而不是单一的per-RoI网络。每个专家都是来自Fast R-CNN的全连接层的相同架构。

Deep Residual Networks

He等人(2015)提出的残差网络(ResNet)由152层组成。ResNet具有较低的误差,并且容易通过残差学习进行训练。更深层次的ResNet可以获得更好的性能。在深度学习领域,人们认为ResNet是一个重要的进步。

Resnet in Resnet

Targ等人(2016)在Resnet in Resnet(RiR)中提出将ResNets和标准卷积神经网络(CNN)结合到深层双流架构中。

ResNeXt

Xie等人(2016)提出了ResNeXt架构。ResNext利用ResNets来重复使用分割-转换-合并策略。

Capsule Networks

Sabour等人(2017)提出了胶囊网络(CapsNet),即一个包含两个卷积层和一个全连接层的架构。CapsNet通常包含多个卷积层,胶囊层位于末端。CapsNet被认为是深度学习的最新突破之一,因为据说这是基于卷积神经网络的局限性而提出的。它使用的是一层又一层的胶囊,而不是神经元。激活的较低级胶囊做出预测,在同意多个预测后,更高级的胶囊变得活跃。在这些胶囊层中使用了一种协议路由机制。Hinton之后提出EM路由,利用期望最大化(EM)算法对CapsNet进行了改进。

Recurrent Neural Networks

循环神经网络(RNN)更适合于序列输入,如语音、文本和生成序列。一个重复的隐藏单元在时间展开时可以被认为是具有相同权重的非常深的前馈网络。由于梯度消失和维度爆炸问题,RNN曾经很难训练。为了解决这个问题,后来许多人提出了改进意见。
Goodfellow等人(2016)详细分析了循环和递归神经网络和架构的细节,以及相关的门控和记忆网络。
Karpathy等人(2015)使用字符级语言模型来分析和可视化预测、表征训练动态、RNN及其变体(如LSTM)的错误类型等。
J´ozefowicz等人(2016)探讨了RNN模型和语言模型的局限性。

RNN-EM

Peng和Yao(2015)提出了利用外部记忆(RNN-EM)来改善RNN的记忆能力。他们声称在语言理解方面达到了最先进的水平,比其他RNN更好。

GF-RNN

Chung等人(2015)提出了门控反馈递归神经网络(GF-RNN),它通过将多个递归层与全局门控单元叠加来扩展标准的RNN。

CRF-RNN

Zheng等人(2015)提出条件随机场作为循环神经网络(CRF-RNN),其将卷积神经网络(CNN)和条件随机场(CRF)结合起来进行概率图形建模。

Quasi-RNN

Bradbury等人(2016)提出了用于神经序列建模和沿时间步的并行应用的准循环神经网络(QRNN)。

Memory Networks

Weston等人(2014)提出了问答记忆网络(QA)。记忆网络由记忆、输入特征映射、泛化、输出特征映射和响应组成。

Dynamic Memory Networks

Kumar等人(2015)提出了用于QA任务的动态记忆网络(DMN)。DMN有四个模块:输入、问题、情景记忆、输出。

Augmented Neural Networks

Olah和Carter(2016)很好地展示了注意力和增强循环神经网络,即神经图灵机(NTM)、注意力接口、神经编码器和自适应计算时间。增强神经网络通常是使用额外的属性,如逻辑函数以及标准的神经网络架构。

Neural Turing Machines

Graves等人(2014)提出了神经图灵机(NTM)架构,由神经网络控制器和记忆库组成。NTM通常将RNN与外部记忆库结合。

Neural GPU

Kaiser和Sutskever(2015)提出了神经 GPU,解决了NTM的并行问题。

Neural Random-Access Machines

Kurach等人(2015)提出了神经随机存取机,它使用外部的可变大小的随机存取存储器。

Neural Programmer

Neelakantan等人(2015)提出了神经编程器,一种具有算术和逻辑功能的增强神经网络。

Neural Programmer-Interpreters

Reed和de Freitas(2015)提出了可以学习的神经编程器-解释器(NPI)。NPI包括周期性内核、程序内存和特定于领域的编码器。

Long Short Term Memory Networks

Hochreiter和Schmidhuber(1997)提出了长短期记忆(Long Short-Term Memory,LSTM),克服了循环神经网络(RNN)的误差回流问题。LSTM是基于循环网络和基于梯度的学习算法,LSTM引入自循环产生路径,使得梯度能够流动。
Greff等人(2017)对标准LSTM和8个LSTM变体进行了大规模分析,分别用于语音识别、手写识别和复调音乐建模。他们声称LSTM的8个变体没有显著改善,而只有标准LSTM表现良好。
Shi等人(2016)提出了深度长短期记忆网络(DLSTM),它是一个LSTM单元的堆栈,用于特征映射学习表示。

Batch-Normalized LSTM

Cooijmans等人(2016)提出了批归一化LSTM(BN-LSTM),它对递归神经网络的隐藏状态使用批归一化。

Pixel RNN

van den Oord等人(2016)提出像素递归神经网络(Pixel-RNN),由12个二维LSTM层组成。

Bidirectional LSTM

W¨ollmer等人(2010)提出了双向LSTM(BLSTM)的循环网络与动态贝叶斯网络(DBN)一起用于上下文敏感关键字检测。

Variational Bi-LSTM

Shabanian等人(2017)提出了变分双向LSTM(Variational Bi-LSTM),它是双向LSTM体系结构的变体。Variational Bi-LSTM使用变分自编码器(VAE)在LSTM之间创建一个信息交换通道,以学习更好的表征。

Googles Neural Machine Translation

Wu等人(2016)提出了名为谷歌神经机器翻译(GNMT)的自动翻译系统,该系统结合了编码器网络、解码器网络和注意力网络,遵循共同的序列对序列(sequence-to-sequence)的学习框架。

Fader Network

Lample等人(2017)提出了Fader网络,这是一种新型的编码器-解码器架构,通过改变属性值来生成真实的输入图像变化。

Hyper Networks

Ha等人(2016)提出的超网络(Hyper Networks)为其他神经网络生成权值,如静态超网络卷积网络、用于循环网络的动态超网络。
Deutsch(2018)使用超网络生成神经网络。

Highway Networks

Srivastava等人(2015)提出了高速路网络(Highway Networks),通过使用门控单元来学习管理信息。跨多个层次的信息流称为信息高速路。

Recurrent Highway Networks

Zilly等人(2017)提出了循环高速路网络(Recurrent Highway Networks,RHN),它扩展了长短期记忆(LSTM)架构。RHN在周期性过渡中使用了Highway层。

Highway LSTM RNN

Zhang等人(2016)提出了高速路长短期记忆Highway Long Short-Term Memory(HLSTM)RNN,它在相邻层的内存单元之间扩展了具有封闭方向连接(即Highway)的深度LSTM网络。

Long-Term Recurrent CNN

Donahue等人(2014)提出了长期循环卷积网络(LRCN),它使用CNN进行输入,然后使用LSTM进行递归序列建模并生成预测。

Deep Neural SVM

Zhang等人(2015)提出了深度神经SVM(DNSVM),它以支持向量机(Support Vector Machine,SVM)作为深度神经网络(Deep Neural Network,DNN)分类的顶层。

Convolutional Residual Memory Networks

Moniz和Pal(2016)提出了卷积残差记忆网络,将记忆机制并入卷积神经网络(CNN)。它用一个长短期记忆机制来增强卷积残差网络。

Fractal Networks

Larsson等人(2016)提出分形网络即FractalNet作为残差网络的替代方案。他们声称可以训练超深度的神经网络而不需要残差学习。分形是简单扩展规则生成的重复架构。

WaveNet

van den Oord等人(2016)提出了用于产生原始音频的深度神经网络WaveNet。WaveNet由一堆卷积层和softmax分布层组成,用于输出。
Rethage等人(2017)提出了一个WaveNet模型用于语音去噪。

Pointer Networks

Vinyals等人(2017)提出了指针网络(Ptr-Nets),通过使用一种称为“指针”的softmax概率分布来解决表征变量字典的问题。


Deep Generative Models

在本节中,我们将简要讨论其他深度架构,它们使用与深度神经网络类似的多个抽象层和表示层,也称为深度生成模型(deep generate Models,DGM)。Bengio(2009)解释了深层架构,例如Boltzmann machines(BM)和Restricted Boltzmann Machines(RBM)等及其变体。
Goodfellow等人(2016)详细解释了深度生成模型,如受限和非受限的玻尔兹曼机及其变种、深度玻尔兹曼机、深度信念网络(DBN)、定向生成网络和生成随机网络等。
Maaløe等人(2016)提出了辅助的深层生成模型(Auxiliary Deep Generative Models),在这些模型中,他们扩展了具有辅助变量的深层生成模型。辅助变量利用随机层和跳过连接生成变分分布。
Rezende等人(2016)开发了一种深度生成模型的单次泛化。

Boltzmann Machines

玻尔兹曼机是学习任意概率分布的连接主义方法,使用最大似然原则进行学习。

Restricted Boltzmann Machines

受限玻尔兹曼机(Restricted Boltzmann Machines,RBM)是马尔可夫随机场的一种特殊类型,包含一层随机隐藏单元,即潜变量和一层可观测变量。
Hinton和Salakhutdinov(2011)提出了一种利用受限玻尔兹曼机(RBM)进行文档处理的深度生成模型。

Deep Belief Networks

深度信念网络(Deep Belief Networks,DBN)是具有多个潜在二元或真实变量层的生成模型。
Ranzato等人(2011)利用深度信念网络(deep Belief Network,DBN)建立了深度生成模型进行图像识别。

Deep Lambertian Networks

Tang等人(2012)提出了深度朗伯网络(Deep Lambertian Networks,DLN),它是一个多层次的生成模型,其中潜在的变量是反照率、表面法线和光源。DLNis是朗伯反射率与高斯受限玻尔兹曼机和深度信念网络的结合。

Generative Adversarial Networks

Goodfellow等人(2014)提出了生成对抗网络(Generate Adversarial Nets,GAN),用于通过对抗过程来评估生成模型。GAN架构是由一个针对对手(即一个学习模型或数据分布的判别模型)的生成模型组成。Mao等人(2016)、Kim等人(2017)对GAN提出了更多的改进。
Salimans等人(2016)提出了几种训练GANs的方法。

Laplacian Generative Adversarial Networks

Denton等人(2015)提出了一种深度生成模型(DGM),叫做拉普拉斯生成对抗网络(LAPGAN),使用生成对抗网络(GAN)方法。该模型还在拉普拉斯金字塔框架中使用卷积网络。

Recurrent Support Vector Machines

Shi等人(2016)提出了循环支持向量机(RSVM),利用循环神经网络(RNN)从输入序列中提取特征,用标准支持向量机(SVM)进行序列级目标识别。


Training and Optimization Techniques

在本节中,我们将简要概述一些主要的技术,用于正则化和优化深度神经网络(DNN)。

Dropout

Srivastava等人(2014)提出Dropout,以防止神经网络过拟合。Dropout是一种神经网络模型平均正则化方法,通过增加噪声到其隐藏单元。在训练过程中,它会从神经网络中随机抽取出单元和连接。Dropout可以用于像RBM(Srivastava et al.,2014)这样的图形模型中,也可以用于任何类型的神经网络。最近提出的一个关于Dropout的改进是Fraternal Dropout,用于循环神经网络(RNN)。

Maxout

Goodfellow等人(2013)提出Maxout,一种新的激活函数,用于Dropout。Maxout的输出是一组输入的最大值,有利于Dropout的模型平均。

Zoneout

Krueger等人(2016)提出了循环神经网络(RNN)的正则化方法Zoneout。Zoneout在训练中随机使用噪音,类似于Dropout,但保留了隐藏的单元而不是丢弃。

Deep Residual Learning

He等人(2015)提出了深度残差学习框架,该框架被称为低训练误差的ResNet。

Batch Normalization

Ioffe和Szegedy(2015)提出了批归一化,通过减少内部协变量移位来加速深度神经网络训练的方法。Ioffe(2017)提出批重归一化,扩展了以前的方法。

Distillation

Hinton等人(2015)提出了将知识从高度正则化模型的集合(即神经网络)转化为压缩小模型的方法。

Layer Normalization

Ba等人(2016)提出了层归一化,特别是针对RNN的深度神经网络加速训练,解决了批归一化的局限性。


Deep Learning frameworks

有大量的开源库和框架可供深度学习使用。它们大多数是为Python编程语言构建的。如Theano、Tensorflow、PyTorch、PyBrain、Caffe、Blocks and Fuel、CuDNN、Honk、ChainerCV、PyLearn2、Chainer、torch等。


Applications of Deep Learning

在本节中,我们将简要地讨论一些最近在深度学习方面的杰出应用。自深度学习(DL)开始以来,DL方法以监督、非监督、半监督或强化学习的形式被广泛应用于各个领域。从分类和检测任务开始,DL应用正在迅速扩展到每一个领域。
例如:

等。
Deng和Yu(2014)提供了DL在语音处理、信息检索、目标识别、计算机视觉、多模态、多任务学习等领域应用的详细列表。
使用深度强化学习(Deep Reinforcement Learning,DRL)来掌握游戏已经成为当今的一个热门话题。每到现在,人工智能机器人都是用DNN和DRL创建的,它们在战略和其他游戏中击败了人类世界冠军和象棋大师,从几个小时的训练开始。例如围棋的AlphaGo和AlphaGo Zero。


Discussion

尽管深度学习在许多领域取得了巨大的成功,但它还有很长的路要走。还有很多地方有待改进。至于局限性,例子也是相当多的。例如:Nguyen等人表明深度神经网络(DNN)在识别图像时容易被欺骗。还有其他问题,如Yosinski等人提出的学习的特征可迁移性。Huang等人提出了一种神经网络攻击防御的体系结构,认为未来的工作需要防御这些攻击。Zhang等人则提出了一个理解深度学习模型的实验框架,他们认为理解深度学习需要重新思考和概括。
Marcus在2018年对深度学习(Deep Learning,DL)的作用、局限性和本质进行了重要的回顾。他强烈指出了DL方法的局限性,即需要更多的数据,容量有限,不能处理层次结构,无法进行开放式推理,不能充分透明,不能与先验知识集成,不能区分因果关系。他还提到,DL假设了一个稳定的世界,以近似方法实现,工程化很困难,并且存在着过度炒作的潜在风险。Marcus认为DL需要重新概念化,并在非监督学习、符号操作和混合模型中寻找可能性,从认知科学和心理学中获得见解,并迎接更大胆的挑战。


Conclusion

尽管深度学习(DL)比以往任何时候都更快地推进了世界的发展,但仍有许多方面值得我们去研究。我们仍然无法完全地理解深度学习,我们如何让机器变得更聪明,更接近或比人类更聪明,或者像人类一样学习。DL一直在解决许多问题,同时将技术应用到方方面面。但是人类仍然面临着许多难题,例如仍有人死于饥饿和粮食危机,癌症和其他致命的疾病等。我们希望深度学习和人工智能将更加致力于改善人类的生活质量,通过开展最困难的科学研究。最后但也是最重要的,愿我们的世界变得更加美好。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>今天在公众号上看到一篇综述论文的翻译,该论文列举出了近年来深度学习的一些重要研究成果,从方法、架构,以及正则化、优化技术方面进行概述。看完之后感 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>今天在公众号上看到一篇综述论文的翻译,该论文列举出了近年来深度学习的一些重要研究成果,从方法、架构,以及正则化、优化技术方面进行概述。看完之后感 @@ -277,13 +277,13 @@ 2020-02-14T14:23:32.000Z 2020-02-15T14:07:31.534Z -

FCNT其实是一个比较老的工作了,性能跟现在是没法比的,但其中的许多进步之处还是非常有价值且值得思考的。事先注明,FCNT中的“FCN”非语义分割中的FCN,FCNT这里的“FCN”指的是全卷积网络。

References

电子文献:
https://www.cnblogs.com/Terrypython/p/10636259.html

参考文献:
[1]Visual Tracking with Fully Convolutional Networks


关注点

不同于以往的工作把CNN看成一个黑盒而不关注不同层的表现,FCNT关注了不同层的功能,即发现:顶层的CNN layer编码了更多的关于语义特征的信息并且可以作为类别检测器;而底层的CNN layer关注了更多局部特征,这有助于将目标从目标中分离出来。这个发现在之后的许多工作中也得到了应用和体现。如下图所示,这里的a图表示的是ground truth,b图表示的是使用VGG的conv4-3,也就是第10层产生的热力图,c图是通过conv5-3也就是第13层产生的热力图。

可以看到,较低维的CNN layer(conv4-3)能够更精准地表示目标的细粒度信息,而较高维的CNN layer(conv5-3)热力图显示较模糊,但对同类别的人也做出了响应。这就是说,顶层缺少类内特征区分,对类间识别比较好,更适合作语义分割;底层则反之,能够更好地表达目标的类内特征和位置信息。
基于不同层(顶层和底层)之间提取特征的不同,作者提出了一种新的tracking方法,利用两种特征相互补充辅助,来处理剧烈的外观变化(顶层特征发挥的作用)和区分目标本身(底层特征发挥的作用)。


稀疏表示

为了简化学习任务,降低模型复杂度,作者采用了稀疏表示的方法。其实基本上等同于字典学习,详见machine-learning笔记:SVD与字典学习
简而言之,字典学习就是寻找一个稀疏矩阵和一个字典矩阵,使它们的乘积尽可能地接近原本的数据。其步骤一般是先求得一个尽可能稀疏的稀疏矩阵,然后固定该稀疏矩阵来更新字典。
在FCNT中,稀疏表示是这样进行的:

注:这里的$\pi$表示的是前景mask,$F$指的是feature map,$c$表示稀疏项。

由于稀疏项$c$已经足够稀疏,我们可以直接省去接近于零的$\lambda \left | c \right |_{1}$。

  1. 首先我们计算前景(也就是目标物体)的误差$e=\underset{i}{min}\left | \pi -Fc_{i} \right |_{2}^{2}$,这一步也就是判断框内有没有目标出现,当$e$小于阈值时,即认为存在目标物体。
  2. 然后计算的是目标物体属于哪一类,即求出使得误差最小的类别的ID,利用公式$id=arg\underset{i}{min}\left | \pi -Fc_{i} \right |_{2}^{2}$。注意这里用了$arg$。

实现

网络结构

由于feature map本身是有内在结构的,有很多的feature map对目标的表达其实并没有起到作用,因此作者设计了一种方法来自动选择高维或者低维CNN上的feature map,同时忽略另一个feature map和噪声。下面就来简单介绍一下该方法的具体步骤,首先来看看FCNT的网络结构。

具体步骤

结合上图,简单介绍一下FCNT的实现流程。

  1. 第一步

    根据给定的target,对VGG的conv4-3和conv5-3进行特征图选择,其目的是选出最相关的特征图。
  2. 第二步

    根据conv5-3的筛选,建立广义的通用网络GNet,用于捕捉目标的类别信息。
  3. 第三步

    根据conv4-3的筛选,建立具有针对性的特定网络SNet,用于将目标从背景中区分出来。
  4. 第四步

    利用第一帧图像来初始化GNet和SNet并进行热力图回归,但要注意两个网络采用不更新的方法。
  5. 第五步

    对于新一帧图像,我们在上一帧目标的位置搜寻RoI,抠取之后送入全卷积网络。
  6. 第六步

    GNet和SNet各自产生一个前景热力图,然后通过最后的干扰检测器选择策略决定使用哪个热力图来确定下一帧的目标位置。

特征图选择

第一步中的特征图选择可能会让人比较疑惑,其实这个选择模型仅用了一个dropout层和一个卷积层,其目标就是使得目标的mask和预测出来的目标热力图尽可能相近。这里把输入的特征图向量化,并用二阶泰勒展开表示特征图扰动,也就是特征图的变化(加入噪声)带来的损失的变化。为了高效地在反向传播中计算,作者仅保留了Hessian矩阵中的对角线上的内容,而忽略泰勒展开式中的其它导数项,也就是保留$h_{ii}$而忽略$h_{ij}$,这就在计算一阶导数和二阶导数是更加高效。

补充:其实mask我之前也是一直不懂的,这里做个补充。
mask中文翻译为“掩模”,但它的功能不仅仅局限于遮掩,总的来说,mask有下面四种用途:

  1. 提取感兴趣区:用预先制作的mask与待处理图像相乘,RoI内图像值保持不变,而区外图像值都变为0。
  2. 屏蔽:用mask对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
  3. 结构特征提取:用相似性变量或图像匹配方法检测和提取图像中与mask相似的结构特征。
  4. 特殊形状图像的制作:其实实现方法一样,只是目的不一。

目标定位

FCNT的定位过程分为两步。

  1. 默认使用GNet

    先把GNet输出的热力图作为目标候选。这是因为GNet使用的是顶层特征,能够更好地处理形变、旋转和遮挡等目标跟踪中的常见问题,举个例子,前面也提到了,顶层特征对画面中同样类别的个体都有反应,这就使得即使目标发生变化,但顶层较为丰富的语义表达依旧能把它判断出来(可能认为是同一类的)。
  2. 判断是否使用SNet

    考虑若画面中出现同类物体时GNet不能很好的处理,因此还需要计算有没有出现目标漂移的情况。其方法是计算在目标候选区域外出现相似目标的概率P,定义一个阈值,若P大于阈值时则认为出现了同类目标,这时候才利用SNet来定位目标的最终位置,是结果更加准确。
]]>
+

FCNT其实是一个比较老的工作了,性能跟现在是没法比的,但其中的许多进步之处还是非常有价值且值得思考的。事先注明,FCNT中的“FCN”非语义分割中的FCN,FCNT这里的“FCN”指的是全卷积网络。

References

电子文献:
https://www.cnblogs.com/Terrypython/p/10636259.html

参考文献:
[1]Visual Tracking with Fully Convolutional Networks


关注点

不同于以往的工作把CNN看成一个黑盒而不关注不同层的表现,FCNT关注了不同层的功能,即发现:顶层的CNN layer编码了更多的关于语义特征的信息并且可以作为类别检测器;而底层的CNN layer关注了更多局部特征,这有助于将目标从目标中分离出来。这个发现在之后的许多工作中也得到了应用和体现。如下图所示,这里的a图表示的是ground truth,b图表示的是使用VGG的conv4-3,也就是第10层产生的热力图,c图是通过conv5-3也就是第13层产生的热力图。

可以看到,较低维的CNN layer(conv4-3)能够更精准地表示目标的细粒度信息,而较高维的CNN layer(conv5-3)热力图显示较模糊,但对同类别的人也做出了响应。这就是说,顶层缺少类内特征区分,对类间识别比较好,更适合作语义分割;底层则反之,能够更好地表达目标的类内特征和位置信息。
基于不同层(顶层和底层)之间提取特征的不同,作者提出了一种新的tracking方法,利用两种特征相互补充辅助,来处理剧烈的外观变化(顶层特征发挥的作用)和区分目标本身(底层特征发挥的作用)。


稀疏表示

为了简化学习任务,降低模型复杂度,作者采用了稀疏表示的方法。其实基本上等同于字典学习,详见machine-learning笔记:SVD与字典学习
简而言之,字典学习就是寻找一个稀疏矩阵和一个字典矩阵,使它们的乘积尽可能地接近原本的数据。其步骤一般是先求得一个尽可能稀疏的稀疏矩阵,然后固定该稀疏矩阵来更新字典。
在FCNT中,稀疏表示是这样进行的:

注:这里的$\pi$表示的是前景mask,$F$指的是feature map,$c$表示稀疏项。

由于稀疏项$c$已经足够稀疏,我们可以直接省去接近于零的$\lambda \left | c \right |_{1}$。

  1. 首先我们计算前景(也就是目标物体)的误差$e=\underset{i}{min}\left | \pi -Fc_{i} \right |_{2}^{2}$,这一步也就是判断框内有没有目标出现,当$e$小于阈值时,即认为存在目标物体。
  2. 然后计算的是目标物体属于哪一类,即求出使得误差最小的类别的ID,利用公式$id=arg\underset{i}{min}\left | \pi -Fc_{i} \right |_{2}^{2}$。注意这里用了$arg$。

实现

网络结构

由于feature map本身是有内在结构的,有很多的feature map对目标的表达其实并没有起到作用,因此作者设计了一种方法来自动选择高维或者低维CNN上的feature map,同时忽略另一个feature map和噪声。下面就来简单介绍一下该方法的具体步骤,首先来看看FCNT的网络结构。

具体步骤

结合上图,简单介绍一下FCNT的实现流程。

  1. 第一步

    根据给定的target,对VGG的conv4-3和conv5-3进行特征图选择,其目的是选出最相关的特征图。
  2. 第二步

    根据conv5-3的筛选,建立广义的通用网络GNet,用于捕捉目标的类别信息。
  3. 第三步

    根据conv4-3的筛选,建立具有针对性的特定网络SNet,用于将目标从背景中区分出来。
  4. 第四步

    利用第一帧图像来初始化GNet和SNet并进行热力图回归,但要注意两个网络采用不更新的方法。
  5. 第五步

    对于新一帧图像,我们在上一帧目标的位置搜寻RoI,抠取之后送入全卷积网络。
  6. 第六步

    GNet和SNet各自产生一个前景热力图,然后通过最后的干扰检测器选择策略决定使用哪个热力图来确定下一帧的目标位置。

特征图选择

第一步中的特征图选择可能会让人比较疑惑,其实这个选择模型仅用了一个dropout层和一个卷积层,其目标就是使得目标的mask和预测出来的目标热力图尽可能相近。这里把输入的特征图向量化,并用二阶泰勒展开表示特征图扰动,也就是特征图的变化(加入噪声)带来的损失的变化。为了高效地在反向传播中计算,作者仅保留了Hessian矩阵中的对角线上的内容,而忽略泰勒展开式中的其它导数项,也就是保留$h_{ii}$而忽略$h_{ij}$,这就在计算一阶导数和二阶导数是更加高效。

补充:其实mask我之前也是一直不懂的,这里做个补充。
mask中文翻译为“掩模”,但它的功能不仅仅局限于遮掩,总的来说,mask有下面四种用途:

  1. 提取感兴趣区:用预先制作的mask与待处理图像相乘,RoI内图像值保持不变,而区外图像值都变为0。
  2. 屏蔽:用mask对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
  3. 结构特征提取:用相似性变量或图像匹配方法检测和提取图像中与mask相似的结构特征。
  4. 特殊形状图像的制作:其实实现方法一样,只是目的不一。

目标定位

FCNT的定位过程分为两步。

  1. 默认使用GNet

    先把GNet输出的热力图作为目标候选。这是因为GNet使用的是顶层特征,能够更好地处理形变、旋转和遮挡等目标跟踪中的常见问题,举个例子,前面也提到了,顶层特征对画面中同样类别的个体都有反应,这就使得即使目标发生变化,但顶层较为丰富的语义表达依旧能把它判断出来(可能认为是同一类的)。
  2. 判断是否使用SNet

    考虑若画面中出现同类物体时GNet不能很好的处理,因此还需要计算有没有出现目标漂移的情况。其方法是计算在目标候选区域外出现相似目标的概率P,定义一个阈值,若P大于阈值时则认为出现了同类目标,这时候才利用SNet来定位目标的最终位置,是结果更加准确。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>FCNT其实是一个比较老的工作了,性能跟现在是没法比的,但其中的许多进步之处还是非常有价值且值得思考的。事先注明,FCNT中的“FCN”非语义分 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>FCNT其实是一个比较老的工作了,性能跟现在是没法比的,但其中的许多进步之处还是非常有价值且值得思考的。事先注明,FCNT中的“FCN”非语义分 @@ -305,13 +305,13 @@ 2020-02-13T15:11:43.000Z 2020-04-14T15:07:23.367Z -

之前在computer-vision笔记:non-max-suppression中已经介绍过IoU。而在2019年又提出了一种新的损失函数计算方式——GIoU,这里就简述一下其motivation和方法。

References

电子文献:
https://zhuanlan.zhihu.com/p/94799295


IoU的问题

  1. IoU与常用的边界框回归损失(smoothL1、均方误差MSE)没有强相关性,即损失相同时,IoU可能会有很大不同。
  2. 如果两个对象不重叠,则IoU值将为零,可此时就无法反映两个边界框彼此之间的距离。如下图所示,绿框与红框、绿框与蓝框的IoU都是零,可显然蓝框与红框的距离是比绿框要大得多的,这里用IoU就无法体现。
  3. IoU还有一个问题就是它无法正确区分两个对象的对齐方式,如下图所示,虽然它们的IoU是相同的,可是对齐方式大不相同,这点IoU也无法体现。

GIoU

针对IoU上述问题,GIoU巧妙地改进了定义的方法,提出了一种更强大的方式。简单来说可分为如下三步:

  1. 寻找两个边界框的最小闭包区域

    如下图所示,寻找A、B两个边界框的最小闭包区域C,一般就是最小外接凸多边形或者圆形。
  2. 计算IoU

    还是用相同的方法,现计算得出IoU。
  3. 计算GIoU

    我们可用如下公式计算GIoU的值用文字来描述,即先计算两个框的最小闭包区域面积,再计算闭包区域中不属于两个框的区域占闭包区域的比重,最后用IoU减去这个比重就得到GIoU。
    类似IoU的损失函数$L_{IoU}=1-IoU$,GIoU的损失函数计算公式如下

效果

实验表明,只需要将边界回归分支的损失修改为GIoU Loss,检测性能可以提升2%-14%,可以说非常引人瞩目了。
在文首给出的参考链接中,也介绍了一种名为DIoU的改进方式,使收敛更加快速,回归更加稳定,这里就不做介绍了。

]]>
+

之前在computer-vision笔记:non-max-suppression中已经介绍过IoU。而在2019年又提出了一种新的损失函数计算方式——GIoU,这里就简述一下其motivation和方法。

References

电子文献:
https://zhuanlan.zhihu.com/p/94799295


IoU的问题

  1. IoU与常用的边界框回归损失(smoothL1、均方误差MSE)没有强相关性,即损失相同时,IoU可能会有很大不同。
  2. 如果两个对象不重叠,则IoU值将为零,可此时就无法反映两个边界框彼此之间的距离。如下图所示,绿框与红框、绿框与蓝框的IoU都是零,可显然蓝框与红框的距离是比绿框要大得多的,这里用IoU就无法体现。
  3. IoU还有一个问题就是它无法正确区分两个对象的对齐方式,如下图所示,虽然它们的IoU是相同的,可是对齐方式大不相同,这点IoU也无法体现。

GIoU

针对IoU上述问题,GIoU巧妙地改进了定义的方法,提出了一种更强大的方式。简单来说可分为如下三步:

  1. 寻找两个边界框的最小闭包区域

    如下图所示,寻找A、B两个边界框的最小闭包区域C,一般就是最小外接凸多边形或者圆形。
  2. 计算IoU

    还是用相同的方法,现计算得出IoU。
  3. 计算GIoU

    我们可用如下公式计算GIoU的值用文字来描述,即先计算两个框的最小闭包区域面积,再计算闭包区域中不属于两个框的区域占闭包区域的比重,最后用IoU减去这个比重就得到GIoU。
    类似IoU的损失函数$L_{IoU}=1-IoU$,GIoU的损失函数计算公式如下

效果

实验表明,只需要将边界回归分支的损失修改为GIoU Loss,检测性能可以提升2%-14%,可以说非常引人瞩目了。
在文首给出的参考链接中,也介绍了一种名为DIoU的改进方式,使收敛更加快速,回归更加稳定,这里就不做介绍了。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>之前在<a href="https://gsy00517.github.io/computer-vision20200128162422/" t + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前在<a href="https://gsy00517.github.io/computer-vision20200128162422/" t @@ -331,13 +331,13 @@ 2020-02-12T14:16:54.000Z 2020-02-14T14:50:08.355Z -

SVD,译为奇异值分解,是一种常用的数据降维方式。一般我们可以对实兑成矩阵作特征值分解,而SVD就类似于对一般情况下MxN的实数矩阵作“特征值分解”,称结果中对角线上的值为奇异值。
而字典学习(Dictionary Learning),又叫KSVD,是一种常用的稀疏表示方法,其本质就是经过K轮迭代,而每次迭代都使用SVD来降维。


分享

SVD
字典学习
关于SVD和字典学习,这位博主的这两篇文章写得很棒,思路清晰准确,尤其是排版让人赏心悦目。自己功力不足,又怕放在收藏夹里吃灰,附链接在此方便日后学习。


代码实现

这里以scipy库中提供的样本图片为例(原本有计算机视觉女生Lena,现换成了一张爬楼梯图),对字典学习的实现过程做一个比较简单的展示和比较详细的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#基本准备
import numpy as np #待会直接调用numpy.linalg.svd函数来实现SVD
from sklearn import linear_model
import scipy.misc #这是一个图像处理的库,待会需借用其中的图片样本
import matplotlib.pyplot as plt #画图要用

#稀疏模型Y = DX,Y为样本矩阵,使用KSVD动态更新字典矩阵D和稀疏矩阵X

class KSVD(object): #单继承object()的属性
def __init__(self, n_components, max_iter = 30, tol = 1e-6,
n_nonzero_coefs = None):
self.dictionary = None
self.sparsecode = None
self.max_iter = max_iter #最大迭代次数
self.tol = tol #稀疏表示结果的容差
self.n_components = n_components #字典所含原子个数(字典的列数)
self.n_nonzero_coefs = n_nonzero_coefs #稀疏度

#初始化字典矩阵
def _initialize(self, y):
u, s, v = np.linalg.svd(y)
self.dictionary = u[:, :self.n_components]

#使用KSVD更新字典的过程
def _update_dict(self, y, d, x):
for i in range(self.n_components):
#选择X中第i行(从0开始)中非零项
index = np.nonzero(x[i, :])[0]
#如果没有非零项,则直接进入下一次for循环
if len(index) == 0:
continue

#更新D中第i列
d[:, i] = 0
#计算误差矩阵
r = (y - np.dot(d, x))[:, index]
#利用SVD的方法,来求解更新字典和稀疏系数矩阵
u, s, v = np.linalg.svd(r, full_matrices = False)
#使用左奇异矩阵的第0列更新字典
d[:, i] = u[:, 0].T
#使用第0个奇异值和右奇异矩阵的第0行的乘积更新稀疏系数矩阵
x[i, index] = s[0] * v[0, :]
return d, x

#KSVD迭代过程
def fit(self, y):
self._initialize(y)
for i in range(self.max_iter): #在最大迭代范围内
#稀疏编码
x = linear_model.orthogonal_mp(self.dictionary, y, n_nonzero_coefs = self.n_nonzero_coefs)
#计算容差
e = np.linalg.norm(y - np.dot(self.dictionary, x))
#满足容差就结束
if e < self.tol:
break
#更新字典
self._update_dict(y, self.dictionary, x)

#稀疏编码
self.sparsecode = linear_model.orthogonal_mp(self.dictionary, y, n_nonzero_coefs = self.n_nonzero_coefs)

return self.dictionary, self.sparsecode


if __name__ == '__main__': #作为脚本时直接执行,但被import至其它脚本时不会被执行
#调用scipy中的爬楼梯图
im_ascent = scipy.misc.ascent().astype(np.float)
ksvd = KSVD(100) #100列的字典
dictionary, sparsecode = ksvd.fit(im_ascent)

plt.figure()
#创建子图,1行2列中的第1张图片
plt.subplot(1, 2, 1)
plt.imshow(im_ascent) #原图
#创建子图,1行2列中的第2张图片
plt.subplot(1, 2, 2)
plt.imshow(dictionary.dot(sparsecode))
#dictionary.dot(sparsecode)等价于numpy.dot(dictionary, sparsecode),即矩阵相乘,这里是DX = Y
plt.show()

以下是字典中“词汇量”(即可表示属性的个数)不同时的三种结果。


]]>
+

SVD,译为奇异值分解,是一种常用的数据降维方式。一般我们可以对实兑成矩阵作特征值分解,而SVD就类似于对一般情况下MxN的实数矩阵作“特征值分解”,称结果中对角线上的值为奇异值。
而字典学习(Dictionary Learning),又叫KSVD,是一种常用的稀疏表示方法,其本质就是经过K轮迭代,而每次迭代都使用SVD来降维。


分享

SVD
字典学习
关于SVD和字典学习,这位博主的这两篇文章写得很棒,思路清晰准确,尤其是排版让人赏心悦目。自己功力不足,又怕放在收藏夹里吃灰,附链接在此方便日后学习。


代码实现

这里以scipy库中提供的样本图片为例(原本有计算机视觉女生Lena,现换成了一张爬楼梯图),对字典学习的实现过程做一个比较简单的展示和比较详细的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#基本准备
import numpy as np #待会直接调用numpy.linalg.svd函数来实现SVD
from sklearn import linear_model
import scipy.misc #这是一个图像处理的库,待会需借用其中的图片样本
import matplotlib.pyplot as plt #画图要用

#稀疏模型Y = DX,Y为样本矩阵,使用KSVD动态更新字典矩阵D和稀疏矩阵X

class KSVD(object): #单继承object()的属性
def __init__(self, n_components, max_iter = 30, tol = 1e-6,
n_nonzero_coefs = None):
self.dictionary = None
self.sparsecode = None
self.max_iter = max_iter #最大迭代次数
self.tol = tol #稀疏表示结果的容差
self.n_components = n_components #字典所含原子个数(字典的列数)
self.n_nonzero_coefs = n_nonzero_coefs #稀疏度

#初始化字典矩阵
def _initialize(self, y):
u, s, v = np.linalg.svd(y)
self.dictionary = u[:, :self.n_components]

#使用KSVD更新字典的过程
def _update_dict(self, y, d, x):
for i in range(self.n_components):
#选择X中第i行(从0开始)中非零项
index = np.nonzero(x[i, :])[0]
#如果没有非零项,则直接进入下一次for循环
if len(index) == 0:
continue

#更新D中第i列
d[:, i] = 0
#计算误差矩阵
r = (y - np.dot(d, x))[:, index]
#利用SVD的方法,来求解更新字典和稀疏系数矩阵
u, s, v = np.linalg.svd(r, full_matrices = False)
#使用左奇异矩阵的第0列更新字典
d[:, i] = u[:, 0].T
#使用第0个奇异值和右奇异矩阵的第0行的乘积更新稀疏系数矩阵
x[i, index] = s[0] * v[0, :]
return d, x

#KSVD迭代过程
def fit(self, y):
self._initialize(y)
for i in range(self.max_iter): #在最大迭代范围内
#稀疏编码
x = linear_model.orthogonal_mp(self.dictionary, y, n_nonzero_coefs = self.n_nonzero_coefs)
#计算容差
e = np.linalg.norm(y - np.dot(self.dictionary, x))
#满足容差就结束
if e < self.tol:
break
#更新字典
self._update_dict(y, self.dictionary, x)

#稀疏编码
self.sparsecode = linear_model.orthogonal_mp(self.dictionary, y, n_nonzero_coefs = self.n_nonzero_coefs)

return self.dictionary, self.sparsecode


if __name__ == '__main__': #作为脚本时直接执行,但被import至其它脚本时不会被执行
#调用scipy中的爬楼梯图
im_ascent = scipy.misc.ascent().astype(np.float)
ksvd = KSVD(100) #100列的字典
dictionary, sparsecode = ksvd.fit(im_ascent)

plt.figure()
#创建子图,1行2列中的第1张图片
plt.subplot(1, 2, 1)
plt.imshow(im_ascent) #原图
#创建子图,1行2列中的第2张图片
plt.subplot(1, 2, 2)
plt.imshow(dictionary.dot(sparsecode))
#dictionary.dot(sparsecode)等价于numpy.dot(dictionary, sparsecode),即矩阵相乘,这里是DX = Y
plt.show()

以下是字典中“词汇量”(即可表示属性的个数)不同时的三种结果。


]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>SVD,译为奇异值分解,是一种常用的数据降维方式。一般我们可以对实兑成矩阵作特征值分解,而SVD就类似于对一般情况下MxN的实数矩阵作“特征值分 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>SVD,译为奇异值分解,是一种常用的数据降维方式。一般我们可以对实兑成矩阵作特征值分解,而SVD就类似于对一般情况下MxN的实数矩阵作“特征值分 @@ -361,13 +361,13 @@ 2020-02-07T07:13:18.000Z 2020-02-09T08:13:04.490Z -

说实话这个寒假真的是挺久的,希望疫情快点好转起来!
之前一直想着要让自己博客中的文章按照更新时间来排序,因为自己在学习的过程中总是会去更新之前的文章,特别是有时候更新量还挺大的,不放到前面来感觉有点可惜哈哈。但是由于没有时间而且不知道怎么做,一直没实现。今天突然想起这个事情,琢磨了好久终于搞出来了。
此次改动之后,我将博客侧栏中的“归档”修改为“最新发布”,依然是按照文章发布时间归档的。


主页文章按更新时间排序

实现前提:使用hexo搭建博客。
我先看了看hexo的配置文件中有没有可以直接设置的地方,试了几个关键词搜索之后发现果然有,这里原本是按照发布日期排序的。

1
2
3
4
5
6
7
8
# Home page setting
# path: Root path for your blogs index page. (default = '')
# per_page: Posts displayed per page. (0 = disable pagination)
# order_by: Posts order. (Order by date descending by default)
index_generator:
path: ''
per_page: 10
order_by: -date

看了一些hexo自动生成的代码之后,我发现了几个与date用法类似的参数,其中updateddate相对应。于是我将-date替换成了-updated,然后hexo start之后发现没起作用。
于是我参照着官方文档里的仅有的一点点说明,改成了-name试试能不能按名称排序,but failed。
没办法,我只好开始了漫长的对所有但凡是包含“date”的文件的阅览过程…网上也找不到任何资料,估计有这种想法的只有我吧哈哈。
终于,我在node_modules\hexo-generator-index-pin-top\lib\generator.js文件中找到了似乎是用于排序的代码,该文件是为了添加文章置顶功能的,但当置顶等级设置相同时,按照发布日期进行排序。
由于本人对JavaScript只是粗略了解,因此依样画葫芦作了一些修改,改完之后该文件中的内容如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
'use strict';
var pagination = require('hexo-pagination');
module.exports = function(locals){
var config = this.config;
var posts = locals.posts;
posts.data = posts.data.sort(function(a, b) {
if(a.top && b.top) { // 当两篇文章top都有定义时
if(a.top == b.top) return b.updated - a.updated; // 若top值一样,则按照文章更新日期降序排列
else return b.top - a.top; // 否则按照top值降序排列
}
else if(a.top && !b.top) { // 以下两种情况是若只有一篇文章top有定义,则将有top的排在前面(这里用异或操作居然不行233)
return -1;
}
else if(!a.top && b.top) { //上一条已解释
return 1;
}
else return b.updated - a.updated; // 若都没定义,则按照文章更新日期降序排列
});
var paginationDir = config.pagination_dir || 'page';
return pagination('', posts, {
perPage: config.index_generator.per_page,
layout: ['index', 'archive'],
format: paginationDir + '/%d/',
data: {
__index: true
}
});
};

再次hexo s,生效,开心。


侧栏按文章更新时间排序

实现前提:使用hexo搭建博客。
这是我在网上找到的另一种方法,由于和next主题不是特别搭配,因此最终没有使用。
首先在侧栏对应的目录下创建一个名为blog-sidebar.swig的渲染文件。(比如我的侧栏在header.swig中编辑,对应的目录为themes\next\layout\_partials,这里我使用了next主题,不同的话自行更改)
在该文件中添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="recent-posts">
<div class="item_headline" style="Text-align:center">
<span>最近更新<span>
</div>
<ul class="posts-list-block">
{% set posts = site.posts.sort('-updated') %}
{% for post in posts.slice('0', '10') %}
<li>
<a href="{{ url_for(post.path) }}" title="{{ post.title }}" target="_blank">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
</div>

然后来到编辑侧栏的文件,在合适的位置添加如下代码。

1
2
3
<div class="blog-sidebar-left">
{% include 'blog-sidebar.swig' %}
</div>

建议添加在nav标签内部,原因我在front-end笔记:使用div实现居中显示一文中曾作说明。


侧栏添加其它文章排序方式

实现前提:使用hexo搭建博客,使用next主题并使用leancloud做站点统计。
实现上面的功能之后,我又添加了“最近阅读”和“热度排名”两项。下面来说说我具体是怎么做的。

注:由于leancloud访问不是很稳定,在线浏览时或许会出错,但下面的方法确实是可行的。

按阅读量排行

在博客目录下执行hexo n page rank新建一个rank页面,编辑其中的index.md文件,添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="rank"></div>
<script src="https://cdn1.lncld.net/static/js/av-core-mini-0.6.4.js"></script>
<script>AV.initialize("YOUR_APPID", "YOUR_APPKEY");</script>
<script type="text/javascript">
var time=0
var title=""
var url=""
var query = new AV.Query('Counter');
query.notEqualTo('id',0);
query.descending('time');
query.limit(10);
query.find().then(function (todo) {
for (var i=0;i<10;i++){
var result=todo[i].attributes;
time=result.time;
title=result.title;
url=result.url;
var content="<p>"+"<font color='#1C1C1C'>"+time+"<i class='fa fa-eye' aria-hidden='true'></i>->"+"</font>"+"<a href='"+"YOUR_URL"+url+"'>"+title+"</a>"+"</p>";
document.getElementById("rank").innerHTML+=content
}
}, function (error) {
console.log("error");
});
</script>

注意,请将上面代码中的YOUR_APPIDYOUR_APPKEY替换为你learncloud中的对应ID和key,可在设置中查看。此外,还要将YOUR_URL替换为你的博客地址,包含协议并以/结束,例如我的:https://Gsy00517.github.io/

注意:请务必注意定义content变量时单引号和双引号的使用。

这时部署的话还是看不到这个page的,要看到排序,还需如下设置。
打开主题配置文件,在对应menu的位置添加rank: /rank/ || signal。这里的图标可以到Font Awesome上去找,非常丰富,我这里用的是signal。
如果你使用了语言包,别忘了到语言包内添加对应的翻译,比如我在hexo\theme\next\languages\zh-Hans.yml文件中对应menu的位置添加了rank: 热度排名

按最近阅读排行

在leancloud中,阅读量最近的更新时间用参数updatedAt表示,这是一个格式为YYYY-MM-DD HH:mm:ss的时间,不过我们也可以对它降序排列。
实现方法于上面的大同小异,这是我调整的代码,可在此基础上创造发挥,亲测有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="new"></div>
<script src="https://cdn1.lncld.net/static/js/av-core-mini-0.6.4.js"></script>
<script>AV.initialize("YOUR_APPID", "YOUR_APPKEY");</script>
<script type="text/javascript">
var updatedAt=""
var title=""
var url=""
var query = new AV.Query('Counter');
query.notEqualTo('id',0);
query.descending('updatedAt');
query.limit(10);
query.find().then(function (todo) {
for (var i=0;i<10;i++){
var result=todo[i].attributes;
title=result.title;
updatedAt=result.updatedAt;
url=result.url;
var content="<p>"+"<a href='"+"YOUR_URL"+url+"' target='_blank'>"+title+"</a>"+"</p>";
document.getElementById("new").innerHTML+=content
}
}, function (error) {
console.log("error");
});
</script>

]]>
+

说实话这个寒假真的是挺久的,希望疫情快点好转起来!
之前一直想着要让自己博客中的文章按照更新时间来排序,因为自己在学习的过程中总是会去更新之前的文章,特别是有时候更新量还挺大的,不放到前面来感觉有点可惜哈哈。但是由于没有时间而且不知道怎么做,一直没实现。今天突然想起这个事情,琢磨了好久终于搞出来了。
此次改动之后,我将博客侧栏中的“归档”修改为“最新发布”,依然是按照文章发布时间归档的。


主页文章按更新时间排序

实现前提:使用hexo搭建博客。
我先看了看hexo的配置文件中有没有可以直接设置的地方,试了几个关键词搜索之后发现果然有,这里原本是按照发布日期排序的。

1
2
3
4
5
6
7
8
# Home page setting
# path: Root path for your blogs index page. (default = '')
# per_page: Posts displayed per page. (0 = disable pagination)
# order_by: Posts order. (Order by date descending by default)
index_generator:
path: ''
per_page: 10
order_by: -date

看了一些hexo自动生成的代码之后,我发现了几个与date用法类似的参数,其中updateddate相对应。于是我将-date替换成了-updated,然后hexo start之后发现没起作用。
于是我参照着官方文档里的仅有的一点点说明,改成了-name试试能不能按名称排序,but failed。
没办法,我只好开始了漫长的对所有但凡是包含“date”的文件的阅览过程…网上也找不到任何资料,估计有这种想法的只有我吧哈哈。
终于,我在node_modules\hexo-generator-index-pin-top\lib\generator.js文件中找到了似乎是用于排序的代码,该文件是为了添加文章置顶功能的,但当置顶等级设置相同时,按照发布日期进行排序。
由于本人对JavaScript只是粗略了解,因此依样画葫芦作了一些修改,改完之后该文件中的内容如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
'use strict';
var pagination = require('hexo-pagination');
module.exports = function(locals){
var config = this.config;
var posts = locals.posts;
posts.data = posts.data.sort(function(a, b) {
if(a.top && b.top) { // 当两篇文章top都有定义时
if(a.top == b.top) return b.updated - a.updated; // 若top值一样,则按照文章更新日期降序排列
else return b.top - a.top; // 否则按照top值降序排列
}
else if(a.top && !b.top) { // 以下两种情况是若只有一篇文章top有定义,则将有top的排在前面(这里用异或操作居然不行233)
return -1;
}
else if(!a.top && b.top) { //上一条已解释
return 1;
}
else return b.updated - a.updated; // 若都没定义,则按照文章更新日期降序排列
});
var paginationDir = config.pagination_dir || 'page';
return pagination('', posts, {
perPage: config.index_generator.per_page,
layout: ['index', 'archive'],
format: paginationDir + '/%d/',
data: {
__index: true
}
});
};

再次hexo s,生效,开心。


侧栏按文章更新时间排序

实现前提:使用hexo搭建博客。
这是我在网上找到的另一种方法,由于和next主题不是特别搭配,因此最终没有使用。
首先在侧栏对应的目录下创建一个名为blog-sidebar.swig的渲染文件。(比如我的侧栏在header.swig中编辑,对应的目录为themes\next\layout\_partials,这里我使用了next主题,不同的话自行更改)
在该文件中添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="recent-posts">
<div class="item_headline" style="Text-align:center">
<span>最近更新<span>
</div>
<ul class="posts-list-block">
{% set posts = site.posts.sort('-updated') %}
{% for post in posts.slice('0', '10') %}
<li>
<a href="{{ url_for(post.path) }}" title="{{ post.title }}" target="_blank">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
</div>

然后来到编辑侧栏的文件,在合适的位置添加如下代码。

1
2
3
<div class="blog-sidebar-left">
{% include 'blog-sidebar.swig' %}
</div>

建议添加在nav标签内部,原因我在front-end笔记:使用div实现居中显示一文中曾作说明。


侧栏添加其它文章排序方式

实现前提:使用hexo搭建博客,使用next主题并使用leancloud做站点统计。
实现上面的功能之后,我又添加了“最近阅读”和“热度排名”两项。下面来说说我具体是怎么做的。

注:由于leancloud访问不是很稳定,在线浏览时或许会出错,但下面的方法确实是可行的。

按阅读量排行

在博客目录下执行hexo n page rank新建一个rank页面,编辑其中的index.md文件,添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="rank"></div>
<script src="https://cdn1.lncld.net/static/js/av-core-mini-0.6.4.js"></script>
<script>AV.initialize("YOUR_APPID", "YOUR_APPKEY");</script>
<script type="text/javascript">
var time=0
var title=""
var url=""
var query = new AV.Query('Counter');
query.notEqualTo('id',0);
query.descending('time');
query.limit(10);
query.find().then(function (todo) {
for (var i=0;i<10;i++){
var result=todo[i].attributes;
time=result.time;
title=result.title;
url=result.url;
var content="<p>"+"<font color='#1C1C1C'>"+time+"<i class='fa fa-eye' aria-hidden='true'></i>->"+"</font>"+"<a href='"+"YOUR_URL"+url+"'>"+title+"</a>"+"</p>";
document.getElementById("rank").innerHTML+=content
}
}, function (error) {
console.log("error");
});
</script>

注意,请将上面代码中的YOUR_APPIDYOUR_APPKEY替换为你learncloud中的对应ID和key,可在设置中查看。此外,还要将YOUR_URL替换为你的博客地址,包含协议并以/结束,例如我的:https://Gsy00517.github.io/

注意:请务必注意定义content变量时单引号和双引号的使用。

这时部署的话还是看不到这个page的,要看到排序,还需如下设置。
打开主题配置文件,在对应menu的位置添加rank: /rank/ || signal。这里的图标可以到Font Awesome上去找,非常丰富,我这里用的是signal。
如果你使用了语言包,别忘了到语言包内添加对应的翻译,比如我在hexo\theme\next\languages\zh-Hans.yml文件中对应menu的位置添加了rank: 热度排名

按最近阅读排行

在leancloud中,阅读量最近的更新时间用参数updatedAt表示,这是一个格式为YYYY-MM-DD HH:mm:ss的时间,不过我们也可以对它降序排列。
实现方法于上面的大同小异,这是我调整的代码,可在此基础上创造发挥,亲测有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="new"></div>
<script src="https://cdn1.lncld.net/static/js/av-core-mini-0.6.4.js"></script>
<script>AV.initialize("YOUR_APPID", "YOUR_APPKEY");</script>
<script type="text/javascript">
var updatedAt=""
var title=""
var url=""
var query = new AV.Query('Counter');
query.notEqualTo('id',0);
query.descending('updatedAt');
query.limit(10);
query.find().then(function (todo) {
for (var i=0;i<10;i++){
var result=todo[i].attributes;
title=result.title;
updatedAt=result.updatedAt;
url=result.url;
var content="<p>"+"<a href='"+"YOUR_URL"+url+"' target='_blank'>"+title+"</a>"+"</p>";
document.getElementById("new").innerHTML+=content
}
}, function (error) {
console.log("error");
});
</script>

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>说实话这个寒假真的是挺久的,希望疫情快点好转起来!<br>之前一直想着要让自己博客中的文章按照更新时间来排序,因为自己在学习的过程中总是会去更新 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>说实话这个寒假真的是挺久的,希望疫情快点好转起来!<br>之前一直想着要让自己博客中的文章按照更新时间来排序,因为自己在学习的过程中总是会去更新 @@ -387,13 +387,13 @@ 2020-02-07T04:24:27.000Z 2020-02-07T05:05:02.611Z -

除了内插值和反卷积(详见computer-vision笔记:上采样和下采样),反池化也是一种上采样方法。由于近些年来有用卷积替代池化的趋势,因此已经比较少用了,但我还是想写一下这种方法和一些自己的思考。


反池化

上图是池化的一个示例,但由于后面要进行反池化,因此我们还需额外把(编码器中的)最大池化层的索引都存储起来,用于之后(解码器中的)的反池化操作。

之后反池化就很简单了,如下图所示,恢复到原始尺寸,将数据映射到索引处,其它地方填充零。


思考

当我刚看到反池化的操作方法时,有点感觉这样在扩充的空间中直接填充零会不会太草率了,为什么不用同一区域做相同的填充或者平均填充呢?
后来想了一想,的确还是直接将最大值映射到原位置,其余地方填充零效果最好。这样尽管会忽略邻近的信息,也就是会丢弃大量低频信息,但是它有助于保持高频信息的完整性。
反过来想,倘若我们将最大值相同地填充至一个区域内(上图中邻近的4格)或者做平均填充,那么不但不能准确地表示高频信息,又不能保证能够表达出低频信息,这就不如上面的做法能保证高频信息的完整性。


池化优点

虽然前面说池化正在逐渐被卷积替代,但池化操作也不是一无是处。与卷积相比,池化有如下优点:

  1. 减少了进行端到端训练的参数量。
  2. 对max-pooling来说,它可以提高边界的勾画,即突出高频信息。
]]>
+

除了内插值和反卷积(详见computer-vision笔记:上采样和下采样),反池化也是一种上采样方法。由于近些年来有用卷积替代池化的趋势,因此已经比较少用了,但我还是想写一下这种方法和一些自己的思考。


反池化

上图是池化的一个示例,但由于后面要进行反池化,因此我们还需额外把(编码器中的)最大池化层的索引都存储起来,用于之后(解码器中的)的反池化操作。

之后反池化就很简单了,如下图所示,恢复到原始尺寸,将数据映射到索引处,其它地方填充零。


思考

当我刚看到反池化的操作方法时,有点感觉这样在扩充的空间中直接填充零会不会太草率了,为什么不用同一区域做相同的填充或者平均填充呢?
后来想了一想,的确还是直接将最大值映射到原位置,其余地方填充零效果最好。这样尽管会忽略邻近的信息,也就是会丢弃大量低频信息,但是它有助于保持高频信息的完整性。
反过来想,倘若我们将最大值相同地填充至一个区域内(上图中邻近的4格)或者做平均填充,那么不但不能准确地表示高频信息,又不能保证能够表达出低频信息,这就不如上面的做法能保证高频信息的完整性。


池化优点

虽然前面说池化正在逐渐被卷积替代,但池化操作也不是一无是处。与卷积相比,池化有如下优点:

  1. 减少了进行端到端训练的参数量。
  2. 对max-pooling来说,它可以提高边界的勾画,即突出高频信息。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>除了内插值和反卷积(详见<a href="https://gsy00517.github.io/computer-vision202002021 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>除了内插值和反卷积(详见<a href="https://gsy00517.github.io/computer-vision202002021 @@ -413,13 +413,13 @@ 2020-02-06T11:02:06.000Z 2020-03-21T09:31:35.895Z -

看paper的时候免不了会遇到一些似懂非懂的缩写,虽然基本上可以跳过,但是总是感觉会卡一下,这里就对我遇到过的做一个汇总,以后就可以哪里不会点哪里了。


拉丁文简写

e.g.

拉丁文exempli gratia,意为比如

etc.

拉丁文et cetera,意为等等

et al.

拉丁文et alia,意为等人

i.e.

拉丁文id est,意为也就是说;换句话说;即

viz.

拉丁文videlicet,意为即;就是

id.

拉丁文idem,意为同上

P.S.

拉丁文post scriptum,意为附言

cf.

拉丁文conf er,意为参看;比较


英文简写

w.r.t.

英文with respect to,意为关于,对于;谈及,谈到

eq.

英文equation,意为方程;等式。(有时也被写作Eqn.)

con.

英文contra,意为相反

const.

英文constant,意为常数;常量

fig.

英文figure,意为图形;数字

s.t.

英文such that,意为于是,因此;使得

st.

英文subject to,意为约束于

i.i.d.

英文independent and identically distributed,概率论术语,意为独立同分布

]]>
+

看paper的时候免不了会遇到一些似懂非懂的缩写,虽然基本上可以跳过,但是总是感觉会卡一下,这里就对我遇到过的做一个汇总,以后就可以哪里不会点哪里了。


拉丁文简写

e.g.

拉丁文exempli gratia,意为比如

etc.

拉丁文et cetera,意为等等

et al.

拉丁文et alia,意为等人

i.e.

拉丁文id est,意为也就是说;换句话说;即

viz.

拉丁文videlicet,意为即;就是

id.

拉丁文idem,意为同上

P.S.

拉丁文post scriptum,意为附言

cf.

拉丁文conf er,意为参看;比较


英文简写

w.r.t.

英文with respect to,意为关于,对于;谈及,谈到

eq.

英文equation,意为方程;等式。(有时也被写作Eqn.)

con.

英文contra,意为相反

const.

英文constant,意为常数;常量

fig.

英文figure,意为图形;数字

s.t.

英文such that,意为于是,因此;使得

st.

英文subject to,意为约束于

i.i.d.

英文independent and identically distributed,概率论术语,意为独立同分布

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>看paper的时候免不了会遇到一些似懂非懂的缩写,虽然基本上可以跳过,但是总是感觉会卡一下,这里就对我遇到过的做一个汇总,以后就可以哪里不会点哪 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>看paper的时候免不了会遇到一些似懂非懂的缩写,虽然基本上可以跳过,但是总是感觉会卡一下,这里就对我遇到过的做一个汇总,以后就可以哪里不会点哪 @@ -439,13 +439,13 @@ 2020-02-04T13:03:05.000Z 2020-02-15T14:07:37.647Z -

之前在看元学习时,就觉得其中的一些想法很适合应用在目标跟踪领域。写完deep-learning笔记:元学习之后我就迫不及待地找了找有没有相关的论文。没想到还真有。这是一篇2018年的paper,我通过谷歌学术查了一下,发现被引次数有五六十次,觉得还是值得阅读一下的。

References

参考文献:
[1]Meta-Tracker: Fast and Robust Online Adaptation for Visual Object Trackers


主要思想

本篇论文主要将Meta Learning运用在了目标模型的初始化上。
首先作者认为结合深度特征和在线学习的模型有两大困难,一是训练的样本不容易获得,二是大多数state-of-the-art的tracker在训练阶段都需要花费大量的时间在初始化上面。
针对上面的难题,作者提出了一种在未来的帧上训练目标模型的思路。这样做有三点好处,一是能使模型更加关注对后续的帧更有价值的特征,二是避免了在当前帧上过拟合,三是能够使初始化更快速。总而言之,就是能保证精度和鲁棒性。
下图是作者运用元学习来初始化跟踪模型的流程,大概有个印象,后面会具体讲算法。

实验结果证明,这种方法能让模型在第一帧初始化是就能很快地提高精度和鲁棒性。(只需迭代一步)

补充:这篇paper多次出现“w.r.t”这个缩写,查了之后才知道就是“with respect to”的意思,也就是“关于,对于”的意思。更多文献常用缩写,见paper笔记:文献中各种缩写汇总


算法

下面直接来看算法,作者直接给了伪代码,挺好懂的。这里的$x$指的是输入,$\theta $指的是模型中的参数,$\alpha $指的是模型梯度下降更新时所用的学习率,$y$指的是我们预测值,而$\widehat{y}$指的是ground truth值。

算法的目的是找出符合元学习目标的合适的$\theta _{0}$和$\alpha $,文中加了星号表示。(对元学习比较模糊的话可以看看我前一篇文章)
算法可以分成两步:第一步使用随机初始化的$\theta _{0}$​和训练样本第一帧的图像进入网络得到一个输出$F$,然后根据ground truth得到loss,再引入一个$\alpha $作为梯度下降的动量参数,反复迭代T次后得到$\theta _{0}^{T}$​作为$\theta _{1}$;接下来第二步,就是看当前得到的参数是否对后面帧(每次迭代随机取一帧)的目标鲁棒,这里用loss对$\theta _{1}$​和$\alpha $的偏导数作为训练的梯度,对N个训练样本得到的梯度求和,用ADAM算法进行优化得到最优的初始参数$\theta _{0}$和$\alpha $。

注意:这里的$\theta _{0}^{T}$不是转置的意思,而是迭代第T次后的$\theta _{0}$。


缺点

善于承认不足是一种良好的品质,作者在文中也来了这样一个转折:“However, it often diverges on longer sequences or the sequences that have very small frame-to-frame variations.”意思是说这种方法在长序列或者目标在帧与帧之间变化不大时表现不佳(会偏离目标)。
回顾一下,当初设计这种方法的目的是模型在接收第一帧之后能够很快地收敛到精度和鲁棒性很好的一组参数位置,这就是说,我们训练得到的$\alpha $相对来说是比较大的,这就导致了它在上述情况下的不稳定。
为此,作者的解决办法是“find a learning rate for subsequent frames and then use existing optimization algorithms to update the models as was done in the original versions of the trackers”,也就是仅用学习到的$\theta _{0}$和$\alpha $来做初始化,然后在随后的在线更新过程中,仍用原来版本的方式进行更新。这里原来的版本指的是CREST和MDNet,作者在这两个tracker的基础上改进出了MetaCREST和MetaSDNet,下面接着讲。


改进CREST

CREST

CREST是一个代表性的结合深度特征使用相关滤波的tracker,它把相关滤波器转换成了一个卷积层,这使得它可以方便地添加新的模块,因为端到端模型的最优化可以用标准的反向传播梯度下降来完成。CREST还引入了时空域的残差模块来避免当物体发生较大变化时目标模型被过分地削弱。

问题

当然,要把作者提出的方法融合到其它的tracker中不是那么轻而易举的,主要有两个问题。

  1. PCA处理与元学习的矛盾

    CREST使用PCA来减少提出的CNN特征的通道数,从512维降到了64维。但是PCA对每一个序列做变换时都会改变基(basis),而元学习希望的是得到一种对每个序列都global的初始化方式,这就很矛盾了。
    于是作者这里用1x1的卷积层替代了CREST中原本的PCA处理,功能还是降维。
    由此一来,要学习的最优初始参数$\theta _{0}$就包含了以下两个:
    1. 降维的参数$\theta _{0_{d}}$。
    2. 相关滤波器的参数$\theta _{0_{f}}$。
  2. 滤波器与元学习的矛盾

    由于滤波器的尺寸大小根据目标的形状和大小一直在变化,而元学习需要的是固定的尺寸。考虑到对目标图像的强行变形会导致效果的严重减弱,作者采用了一种名为canonical size initialization的方法,也就是设置尺寸和比例为训练数据集的平均值以尽可能地减小对最终效果的影响。

MetaCREST

最终改进之后的MetaCREST框架如下图所示。

可以看一下它与原tracker的效果对比。

可以看到还是有一定进步的。


改进MDNet

MDNet

MDNet的特点就是在训练阶段使用了multi-domain的训练方式,这是它提升鲁棒性的关键。此外,它还使用了dropout和不同层之间不同的学习率来防止过拟合。

MetaSDNet

然而,作者用元学习把上述的multi-domain训练方式和正则化技术都替代了,仅用元学习来完成可以快速适应且鲁棒的分类器。

作者随后给出了两者的效果比较,可是好像并没有看到任何的明显的进步,两者效果几乎一样。

其实文章之所以强调MDNet是因为它初始化太慢了,在效果相同的情况下,MetaSDNet的初始化速度在MDNet的基础上加快了大约30倍。此外,在没有使用multi-domain训练方式和正则化技术的情况下,用元学习来达到相近的效果,也不能不说元学习的能力还是值得肯定的。


感想

自己接触元学习的相关算法之后,总想着直接套用到别的任务上去。这篇论文和我的想法有点类似,但作者在实际实现时肯定用了更多的方法来解决各种各样的难题。而这篇文章也只能说是用元学习的思想做了一些探索,引用作者的一部分总结:
“Other than target appearance modeling, which is the focus of this paper, there are many other important factors in object tracking algorithms. For example, when or how often to update the model, how to manage the database, and how to define the search space. These considerations are sometimes more important than target appearance modeling. In future work we propose including handling of these as part of learning and meta-learning.”
总而言之,还有很长的路要走~

]]>
+

之前在看元学习时,就觉得其中的一些想法很适合应用在目标跟踪领域。写完deep-learning笔记:元学习之后我就迫不及待地找了找有没有相关的论文。没想到还真有。这是一篇2018年的paper,我通过谷歌学术查了一下,发现被引次数有五六十次,觉得还是值得阅读一下的。

References

参考文献:
[1]Meta-Tracker: Fast and Robust Online Adaptation for Visual Object Trackers


主要思想

本篇论文主要将Meta Learning运用在了目标模型的初始化上。
首先作者认为结合深度特征和在线学习的模型有两大困难,一是训练的样本不容易获得,二是大多数state-of-the-art的tracker在训练阶段都需要花费大量的时间在初始化上面。
针对上面的难题,作者提出了一种在未来的帧上训练目标模型的思路。这样做有三点好处,一是能使模型更加关注对后续的帧更有价值的特征,二是避免了在当前帧上过拟合,三是能够使初始化更快速。总而言之,就是能保证精度和鲁棒性。
下图是作者运用元学习来初始化跟踪模型的流程,大概有个印象,后面会具体讲算法。

实验结果证明,这种方法能让模型在第一帧初始化是就能很快地提高精度和鲁棒性。(只需迭代一步)

补充:这篇paper多次出现“w.r.t”这个缩写,查了之后才知道就是“with respect to”的意思,也就是“关于,对于”的意思。更多文献常用缩写,见paper笔记:文献中各种缩写汇总


算法

下面直接来看算法,作者直接给了伪代码,挺好懂的。这里的$x$指的是输入,$\theta $指的是模型中的参数,$\alpha $指的是模型梯度下降更新时所用的学习率,$y$指的是我们预测值,而$\widehat{y}$指的是ground truth值。

算法的目的是找出符合元学习目标的合适的$\theta _{0}$和$\alpha $,文中加了星号表示。(对元学习比较模糊的话可以看看我前一篇文章)
算法可以分成两步:第一步使用随机初始化的$\theta _{0}$​和训练样本第一帧的图像进入网络得到一个输出$F$,然后根据ground truth得到loss,再引入一个$\alpha $作为梯度下降的动量参数,反复迭代T次后得到$\theta _{0}^{T}$​作为$\theta _{1}$;接下来第二步,就是看当前得到的参数是否对后面帧(每次迭代随机取一帧)的目标鲁棒,这里用loss对$\theta _{1}$​和$\alpha $的偏导数作为训练的梯度,对N个训练样本得到的梯度求和,用ADAM算法进行优化得到最优的初始参数$\theta _{0}$和$\alpha $。

注意:这里的$\theta _{0}^{T}$不是转置的意思,而是迭代第T次后的$\theta _{0}$。


缺点

善于承认不足是一种良好的品质,作者在文中也来了这样一个转折:“However, it often diverges on longer sequences or the sequences that have very small frame-to-frame variations.”意思是说这种方法在长序列或者目标在帧与帧之间变化不大时表现不佳(会偏离目标)。
回顾一下,当初设计这种方法的目的是模型在接收第一帧之后能够很快地收敛到精度和鲁棒性很好的一组参数位置,这就是说,我们训练得到的$\alpha $相对来说是比较大的,这就导致了它在上述情况下的不稳定。
为此,作者的解决办法是“find a learning rate for subsequent frames and then use existing optimization algorithms to update the models as was done in the original versions of the trackers”,也就是仅用学习到的$\theta _{0}$和$\alpha $来做初始化,然后在随后的在线更新过程中,仍用原来版本的方式进行更新。这里原来的版本指的是CREST和MDNet,作者在这两个tracker的基础上改进出了MetaCREST和MetaSDNet,下面接着讲。


改进CREST

CREST

CREST是一个代表性的结合深度特征使用相关滤波的tracker,它把相关滤波器转换成了一个卷积层,这使得它可以方便地添加新的模块,因为端到端模型的最优化可以用标准的反向传播梯度下降来完成。CREST还引入了时空域的残差模块来避免当物体发生较大变化时目标模型被过分地削弱。

问题

当然,要把作者提出的方法融合到其它的tracker中不是那么轻而易举的,主要有两个问题。

  1. PCA处理与元学习的矛盾

    CREST使用PCA来减少提出的CNN特征的通道数,从512维降到了64维。但是PCA对每一个序列做变换时都会改变基(basis),而元学习希望的是得到一种对每个序列都global的初始化方式,这就很矛盾了。
    于是作者这里用1x1的卷积层替代了CREST中原本的PCA处理,功能还是降维。
    由此一来,要学习的最优初始参数$\theta _{0}$就包含了以下两个:
    1. 降维的参数$\theta _{0_{d}}$。
    2. 相关滤波器的参数$\theta _{0_{f}}$。
  2. 滤波器与元学习的矛盾

    由于滤波器的尺寸大小根据目标的形状和大小一直在变化,而元学习需要的是固定的尺寸。考虑到对目标图像的强行变形会导致效果的严重减弱,作者采用了一种名为canonical size initialization的方法,也就是设置尺寸和比例为训练数据集的平均值以尽可能地减小对最终效果的影响。

MetaCREST

最终改进之后的MetaCREST框架如下图所示。

可以看一下它与原tracker的效果对比。

可以看到还是有一定进步的。


改进MDNet

MDNet

MDNet的特点就是在训练阶段使用了multi-domain的训练方式,这是它提升鲁棒性的关键。此外,它还使用了dropout和不同层之间不同的学习率来防止过拟合。

MetaSDNet

然而,作者用元学习把上述的multi-domain训练方式和正则化技术都替代了,仅用元学习来完成可以快速适应且鲁棒的分类器。

作者随后给出了两者的效果比较,可是好像并没有看到任何的明显的进步,两者效果几乎一样。

其实文章之所以强调MDNet是因为它初始化太慢了,在效果相同的情况下,MetaSDNet的初始化速度在MDNet的基础上加快了大约30倍。此外,在没有使用multi-domain训练方式和正则化技术的情况下,用元学习来达到相近的效果,也不能不说元学习的能力还是值得肯定的。


感想

自己接触元学习的相关算法之后,总想着直接套用到别的任务上去。这篇论文和我的想法有点类似,但作者在实际实现时肯定用了更多的方法来解决各种各样的难题。而这篇文章也只能说是用元学习的思想做了一些探索,引用作者的一部分总结:
“Other than target appearance modeling, which is the focus of this paper, there are many other important factors in object tracking algorithms. For example, when or how often to update the model, how to manage the database, and how to define the search space. These considerations are sometimes more important than target appearance modeling. In future work we propose including handling of these as part of learning and meta-learning.”
总而言之,还有很长的路要走~

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>之前在看元学习时,就觉得其中的一些想法很适合应用在目标跟踪领域。写完<a href="https://gsy00517.github.io/de + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前在看元学习时,就觉得其中的一些想法很适合应用在目标跟踪领域。写完<a href="https://gsy00517.github.io/de @@ -467,13 +467,13 @@ 2020-02-03T08:22:45.000Z 2020-03-27T03:14:03.324Z -

元学习(Meta Learning)即让机器学习如何去学习(learn to learn)。我前几天看到这个概念,觉得非常有意思,这几天也一直在看相关的概念和implement,发现其实很多方法或者思想已经在最新的一些工作中得到有意或者无意运用。因此我决定对我目前的了解做一个总结,或许能给之后的思考带来启发。
强烈推荐台湾教授李宏毅关于Meta Learning的课程录像,油管上有资源,总共4个part,讲得真的非常清楚!
此外,斯坦福在2019年秋季也推出了多任务学习和元学习的CS330,感兴趣可以看一看,语速稍快,可以锻炼一下英语。
由于目前对这个领域的了解还不够深入,分类可能不是很恰当,以后待理解加深之后再做调整,如有错误还请见谅。

References

电子文献:
https://cloud.tencent.com/developer/article/1463397
https://zhuanlan.zhihu.com/p/28639662
https://mp.weixin.qq.com/s/MApZS0-SL4FNP2kmavhcxQ
https://blog.csdn.net/liuglen/article/details/84770069
https://blog.csdn.net/mao_feng/article/details/78939864
https://blog.csdn.net/ppp8300885/article/details/80383246


元学习

人工智能的一个基本问题是它无法像人类一样高效地学习。
与人类相比,大多数最先进的深度学习方法都有两个关键的弱点:

  1. 样本效率低

    深度学习的样本效率很差,学习效率很低下。尽管目前已经有许多深度学习模型显示了超人的表现,但达到这种效果需要数百万个训练样本来训练。也就是说对one-shot learning和few-shot learning的能力还是不够强。
  2. 可移植性差

    许多模型不会从以前的经验或学到的知识中学习。也就是说训练得到的知识是不共享的,并且每个任务都独立于其他任务进行训练。

基于以上这些问题的考虑,“元学习”被提出,它研究的不是如何提升模型解决某项具体的任务(分类、回归、检测)的能力,而是研究如何提升模型解决一系列任务的能力。它是增强学习之后的一个研究分支,总的来看,人工智能的研究呈现出了从Artificial Intelligence,到Machine Learning,到Deep Learning,到Deep Reinforcement Learning,再到Deep Meta Learning这样一个大体的方向,这样下去或许最后就真的能够创造出自己学习的机器了。也就是说:
在过去,我们不仅知道机器是怎么想的,而且知道机器是怎么学的。
目前,我们虽然不知道机器是怎么想的,但是知道机器是怎么学的。
未来,我们非但不知道机器是怎么想的,还不知道机器是怎么学的。
也就是从人工智能到智能。
哈哈上面扯远了,前面说过,我们可以将元学习定义为“学习如何学习”。但是实际上,我们还不知道确切的定义或解决方案。因此,它仍然是一个宽松的术语,指的是不同的方法。比如,从某种意义上来说,我们可以认为迁移学习也是一种元学习。它利用别的任务中训练得比较好的参数,使得在本任务得数据集上能够更好更快地达到最优。
一般可以将Meta Learning分成两个阶段:

  1. Gradual Learning

    也就是通过在一些相似任务上训练得到Meta Learner,它具有较强的泛化能力。
  2. Rapid Learning

    也就是利用该Meta Learner直到之后的学习过程,使模型能够快速调整以适应新的任务。

在李宏毅的课程中提到,Machine Learning其实就是学习如何根据数据找出一个$f$函数,使得在测试中,该函数能根据输入获得正确的输出。而Meta Learning其实就是找到一个$F$函数,这个$F$函数的能力是找出$f$函数,也就是说,不同于机器学习直接找$f$,元学习中机器的任务是去找这个$F$。
来看下图,这里说的其实就是一个训练的过程,通过训练找出最终模型中效果最好的参数$\widehat{\theta }$。

可以看上图红框圈出的部分,仔细想想,这些部分的不同也就构成了不同的算法。然而,目前这些红框中的结构基本上还是人为设计定义的。那么元学习思考的就是能不能让机器自己学习其中的方法与结构。


数据集划分

由于元学习学习的是解决一系列任务的能力,因此不同于以往的将数据集划分成训练集和测试集(或许还有验证集),在元学习中,我们将数据集分割成Training Tasks、Testing Tasks(、Validaiton Tasks)。每一个任务都包含一个训练集和一个测试集。为了区别,这里称这些小训练集为Support Set,称这些小测试集为Query Set。


元学习与one-shot learning

一般来说,元学习总是与one-shot learning相结合的。我们知道,目前许多单任务模型的训练都是以天计数的。而元学习的训练集中每一组数据都是一个任务,假如Training Tasks中有n个任务,那就是说我们要训练n次也就是单任务模型的训练时间的n倍,这也意味着我们需要很长的时间才能检验一次效果,显然是不划算的。
而one-shot learning虽然只提供了少量的数据,但这也是它的一个优势——训练更快,因此我们对one-shot learning任务进行元学习,可以在较短的时间内看到训练效果。


学习初值的元学习

上文说过可以让机器自己学习红框中的结构与方法,这里有元学习中两种代表性的方法MAML和Reptile,它们都学习的是初始化的方式(上图第二个红框)。MAML音同mammal(哺乳动物),也是Model Agnostic Meta Learning的缩写;而Reptile(意为爬行动物),由于找不到缩写的来源,严重怀疑是故意这么命名来与MAML对应。
它们两者实际上学习的是该把初始化参数$\phi $设成什么,其损失函数为

这里的$\widehat{\theta }^{n}$其实就是在第n个任务中,由初始参数$\phi $通过Support Set学到的最终模型中的参数。而$l^{n}\left ( \widehat{\theta }^{n} \right )$就是对于第n个任务在Query Set上的损失。
首先我借用李宏毅老师给的例子来弄明白这里“学习把参数$\phi $初始化成什么”到底与普通的训练中“学习把参数训练成什么”有什么区别。

在上图中,左图表示的是我们一般意义上的训练(Model Pre-training),右图表示的是MAML或者Reptile中的初始化。可以看到,左图最终训练得出的参数$\phi $在task 1(Training Tasks)上表现得最好,但在一个不同的任务task 2(Testing Tasks)上,它只能收敛到局部最优点(local minima)。在右图中,虽然在Training Tasks上表现得不是最好,但是在遇到新的任务训练时,现在的初始化参数$\phi $可以很快很好地收敛到全局最优点(global minima)。
也就是说,一般意义上的训练,我们关注的是在所有Training Tasks上达到最好的情况,比如找出让训练时的损失函数最小的一组参数,但我们并不关注拿此时的参数去新任务上进行训练是否会得到好的结果;而在学习初始化的元学习中,我们不在意$\phi $在Training Tasks中表现得有多好,我们在意的是拿初始化后的$\phi $到下一个任务上去训练能够很快很好地取得不错的表现。简而言之,Model Pre-training关注的是$\phi $当前的表现,而Meta Learning关注的是$\phi $未来遇到新任务时的潜力。如果说到这里还不是很清楚,后文还有直观的理解。
具体怎么找到这个$\phi $呢?这就产生了下面两种方法。

  1. MAML模型无关元学习

    MAML考虑的是训练出一个初始化参数$\phi $,使得拥有这个参数的模型在遇到一个新的任务时能够一步到位达到最好,比如接收到一张新的图片就可以快速训练成为辨别该图片同一类别图片的最优模型。
    当然,寻找这个$\phi $的方法还是梯度下降,因此也要求$\phi $关于损失函数$L$的偏导数。由于MAML考虑的是模型在遇到新任务时一步到位,因此$\widehat{\theta }$也仅进行一次梯度下降,如下所示。 由于这里$\widehat{\theta }_{j}$对$\phi _{i}$的偏导数还是要分类讨论很麻烦,因此作者使用了a first-order approximation,如下图所示。 也就是当且仅当$i$等于$j$的时候时取$\widehat{\theta }_{j}$对$\phi _{i}$的偏导数为1,否则取偏导数为0。
    我们可以来直观地看一下MAML在做什么。如下图所示,左为MAML,右为Model Pre-training,上文已比较过两者关注点的不同,这里再看看两者方法上的不同。 MAML首先在第一个任务采样出来的样本上计算损失函数并进行一次梯度下降,然后用此时的参数计算损失函数再进行一次梯度下降,最后,根据第二次梯度下降的梯度方向,平行地对原参数$\phi $进行一次梯度下降。注意,这里每一次梯度下降使用的学习率不需要一样。可以这样理解,第二次梯度下降后的参数相比第一次梯度下降后的参数在task m上表现要好,因此$\phi $沿相同的方向梯度下降,就能使在遇到task m的时以更少的训练次数到达离较优的第二次梯度下降后的参数更近的位置。在task n上同理。
    而Model Pre-training,则是要求在训练的每个任务上尽可能做到最好,于是直接朝着由每个任务采样样本得到的损失函数计算出的梯度的方向靠近。
    其实,Model Pre-training的基本思想就是每训练完一个任务,我们就把这过程中的信息用来更新模型。而Meta Learning的基本思想就是找到所有学习任务背后的基础知识。因此,我们不会立即更新模型,而是等待一批任务完成。稍后,我们将从这些任务中学到的所有知识合并到一个更新中。 个人感觉,这有点像想要去做、却为了大局而保持一点克制的感觉,似乎的确蕴含了一点点人生哲理在里面。
  2. Reptile一阶元学习

    Reptile的思想其实是和MAML一样的,只不过它使用了多次梯度下降后的方向来更新$\phi _{i}$,也就是对遇到一个新任务时的更新次数不加限制,不指望一步到位。 我们可以通过Pre-training、MAML和Reptile的对比图来理解一下Reptile改进的思想,它相当于在当下的任务和未来的任务之间寻找一个平衡。 我觉得就泛化能力而言,MAML可能还是要好于Reptile的。
    实际上,在MAML的使用中,我们也可以进行多次梯度下降,训练时假设一步到位并不意味着在实际使用的时候只能使用一次梯度下降。

李宏毅老师在课程中还给出了一个toy example的例子,区分Model Pre-training和Meta Learning。如下所示,toy example的目标是拟合正弦曲线

这里的$a$和$b$是任取的随机数,也就是每个任务都是拟合一个不同的正弦曲线。
来看看两种方法的训练结果,其中左边是Model Pre-training的结果,右边是Meta Learning的结果,这里使用的是MAML方法。

图中绿色的线就是训练好之后的参数构成的模型。可以看到,Model Pre-training为了在训练集中各种正弦曲线中达到损失函数最小,训练结果为一条直线,这在测试时效果很差;而MAML则预备在遇到一个新的曲线时能很快地去拟合,在测试时能很快地达到很好的效果。
然而,目前这类方法的效果好像仅在多样性较低的任务分布中进行实验得到了验证,在这些任务上做得好不代表在复杂程度不同、模式不同的任务上也可以有很好的表现。对于应用在高复杂度的任务上的效果可能还是要打个问号的,在这方面的研究还有很长的路要走。

补充:其实这两种方法后面还是有比较复杂的数学推导的,我在网上看到一个博客写得挺好,补充在这里MAML-模型无关元学习算法Reptile-一阶元学习算法


学习更新的学习

上一节介绍的是如何让机器自己学习出一个初始化的方法,前文圈出的红框中另有一项是Update,那么我们是否可以学习如何更新模型呢?

借鉴LSTM

之前没有看过LSTM,所以我想顺便也理一下LSTM的基本思路。

相比于RNN,LSTM从前一级获得的输入有两个(也可以看作将一个输入拆成两段),区别就在于$c$更新得更慢,$h$更新得更快。

这里各个参数表示的含义如下所示,$\sigma $表示的是某种函数。

每一块的输出和传递给下一级的$c$与$h$计算方法如下所示,这里表示的是点乘操作。

  1. 方法1

    一般而言,我们梯度下降使用的是如下公式:对比LSTM中$c$的的更新公式$c^{t}=z^{f}\odot c^{t-1}+z^{i}\odot z$,我们很容易发现两者的结构有相通之处,由此产生灵感,我们是否可以把这里的$z^{f}$和$z^{i}$也变成可以学习的呢。也就是输入$\theta ^{t-1}$和$-\bigtriangledown _{\theta }l$,不人为设定$z^{f}$和$z^{i}$,直接输出$\theta ^{t}$。
    于是就有了如下方法。 但是,由于$-\bigtriangledown _{\theta }l$的计算依赖于$\theta ^{t-1}$(上图虚线表示),这在反向传播时会比较复杂,因此为了简化,在反向传播时不考虑$-\bigtriangledown _{\theta }l$对$\theta ^{t-1}$的依赖。 注意,如果对所有的参数都设计这样一种学习如何更新的方式,那么计算量是非常大的,因此我们往往对所有参数采用同一套学习出来的更新方式。这是合理的,因为往往我们也会对所有参数设置同样的学习率。
    作者对此进行实验来看一看$z^{f}$和$z^{i}$的学习结果究竟如何。如下所示,可见$z^{f}$最后还是收敛到了1,而$z^{i}$也集中在1附近,但也的确学到了一些不一样的信息。 该实验可以表明,我们一般使用的梯度下降公式是比较合理的。不过这里元学习的方法并不是没有作用,后面会提到它的效果。
  2. 方法2

    为了利用更多以往的“记忆”,也可以设计一种两层的LSTM,如下图所示。 实际上,LSTM这种循环序列模型本身就有利用过往记忆的功能,我将在下一节做更多的介绍。
  3. 效果

    其实这些方法还是比较难实现的,这里来看一下利用LSTM来更新参数最后的效果。 可以看到,这种方法在各种优化算法面前还是有明显的优势的。这里有一个问题是作者进行的实验都是在MNIST数据集上进行的,比如说这里Meta Learning的Training Tasks是识别0,1,2,3…7,Testing Tasks是识别8,9。其实这些任务是同一类任务。作者也注意到了这一点,因此他在训练时用的是20units,测试时使用了40units;训练时只用了1个layer,测试时用了2个layer。以此来证明利用LSTM来更新参数这种方法在不同的模型中的能力。

    预测梯度

    既然Meta Learning的目的是实现快速学习,而快速学习的关键就是神经网络的梯度下降要准且快,那么是否可以让神经网络利用以往的任务学习如何预测梯度,这样面对新的任务,只要梯度预测得准,那么学习得就会更快。
    在Learning to Learn by Gradient Descent by Gradient Descent一文中(好好品味这个题目),作者提出了这样一种方法,即训练一个通用的神经网络来预测梯度,用一次二次方程的回归问题来训练,这种方法得到的神经网络优化算法比Adam、RMSProp还要好。

学习记忆的学习

元学习其实也可以利用以往的经验来学习,于是这又诞生了一系列模型。

记忆增强神经网络

一般的神经网络不具有记忆功能,输出的结果只基于当前的输入。而LSTM网络的出现则让网络有了记忆,它能够根据之前的输入给出当前的输出。但是,LSTM的记忆程度并不是那么理想,对于比较长的输入序列,LSTM的最终输出只与最后的几步输入有关,也就是long dependency问题,当然这个问题可以由注意力机制解决,然而还是不能从根本上解决长期记忆的问题,原因是由于LSTM是假设在时间序列上的输入输出:由t-1时刻得到t时刻的输出,然后再循环输入t时刻的结果得到t+1时刻的输出,这样势必会使处于前面序列的输入被淹没,导致这部分记忆被“丢掉”。
基于以上问题,研究者们提出了神经图灵机等记忆增强神经网络模型。我们知道,人除了用脑子记,还会使用做笔记等方式做辅助,当我们忘记时,我们可以去看笔记来获得相关的记忆。记忆增强神经网络的思想就是如此,它让所有的笔记的“读”“写”操作都可微分化,因此可以用神经网络误差后向传播的方式去训练模型。
在记忆增强神经网络中,我们引入外部存储器来存储样本表示和类标签信息。以Meta-Learning with Memory-augmented Neural Networks这篇文章为例,我们看一下它的网络结构。

我们可以看到,网络的输入把上一次的y label也作为输入,并且添加了external memory存储上一次的x输入,这使得下一次输入后进行反向传播时,可以让y label和x建立联系,使得之后的x能够通过外部记忆获取相关图像进行比对来实现更好的预测。

WaveNet

WaveNet的网络每次都利用了之前的数据,因此可以考虑WaveNet的方式来实现Meta Learning。

SNAIL

SNAIL意为蜗牛(为什么这么多动物名…),其实直接把论文标题缩写之后和SNAIL是不一样的,之所以命名成SNAIL可能是因为网络结构有点像蜗牛吧。

  1. 时序卷积

    时序卷积是通过在时间维度上膨胀的一维卷积生成时序数据的结构,如下图所示。 这种时序卷积是因果的,所以在下一个时间节点生成的值只会被之前时间节点的信息影响,而不受未来信息的影响。相比较于传统的RNN,它提供了一种更直接,更高带宽的方式来获取过去的信息。但是,为了处理更长的序列,膨胀率通常是指数级增长的,所以需要的卷积层数和序列长度呈对数关系。因此,只能对很久之前的信息进行粗略的访问,有限的容量和位置依赖性对于元学习方法是不利的,不能充分利用大量的先前的经验。
  2. 注意力模块

    注意力模块可以让模型在可能的无限大的上下文中精确的定位信息,把上下文信息当做无序的键值对,通过内容对其进行查找。
  3. 网络

    SNAIL由两个时序卷积和attention交错组成。时序卷积可以在有限的上下文中提供高带宽的访问方式,attention可以在很大的上下文中精确地访问信息,所以将二者结合起来就得到了SNAIL。在时序卷积产生的上下文中应用causal attention,可以使网络学习应该挑选聚集哪些信息,以及如何更好地表示这些信息。

    注意:英语小贴士,因果:causal;休闲:casual。


学习方法的学习

元学习还用于对数据处理中的一些方法进行学习,比如这里要介绍的对特征提取方法的学习。

Siamese Network孪生网络

Siamese Network中文译为孪生网络。常用于人脸验证(不同于人脸识别),近几年在目标跟踪领域也大放光彩。
在Siamese Network中,我们首先使用两个共享模型参数值的相同网络来提取两个样本的特征(也有个别伪Siamese Network,它们不共享网络权重)。然后,我们将提取出的特征输入鉴别器,判断两个样本是否属于同一类对象。例如,我们可以计算其特征向量的余弦相似度(p)。如果它们相似,那么p应该接近1;否则,它们应该接近0。根据样本的标签和p,我们对网络进行相应的训练。简而言之,我们希望找到使样例属于同一类或将它们区分开来的特性。

这里利用神经网络的好处是它能够学习到哪些特征是该提取的,比如在一些传统的方法中,它们会对整个图像进行特征转化,其中也将包括背景,这样就可能会导致当两个相似背景但不同对象的图片作为输入时,输出会判定为相同。而神经网络它可以在训练过程中学习到哪些部分是值得关注的,可以学会ignore背景。
李宏毅老师在讲这一块的时候还提到了一种思路,就是学习一种算法(生成网络),这种算法能够使用one-shot learning输入的少量样本生成许多样本用于和prediction的那个输入进行比对验证,其框架与Siamese Network基本相似。

Matching Network匹配网络

Matching Network与Siamese Network十分相似。我们知道,人的注意力是可以利用以往的经验来实现提升的。那么,能不能利用以往的任务来训练一个attention模型,使得模型面对新的任务时,能够直接关注最重要的部分。
于是就有了Matching Network。

这里的g和f是特征提取器,和Siamese Network一样使用深度网络来提取特征,用于我们的输入和测试样本。通常情况下,g和f是相同的,共享相同的深度网络。然后,我们比较它们的相似度。同样,我们从预测中计算一个损失函数来训练我们的特征提取器。
这里构造了一个attention机制,也就是最后的label判断是通过attention的叠加得到的:

其实这里的网络结构远比上面画出来的复杂(用到了LSTM)。我们可以来看一下它的表现,在Omniglot数据集(找一批人照着象形文字画出来的)上,它取得了比Siamese Network更好一点的成绩。不过具体能力如何,还得看论文跑实验。

Prototypical Network原型网络

该方法思想十分简单,效果也非常好。它学习的是一个度量空间,通过计算和每个类别的原型表达的距离来进行分类。
具体的思路是:每个类别都存在一个聚在某单个原型表达周围的embedding,该类的原型是Support Set在embedding空间中的均值。然后,分类问题变成在embedding空间中的最近邻。如图所示,c1、c2、c3分别是三个类别的均值中心(称Prototype),将测试样本x进行embedding后,与这3个中心进行距离计算,从而获得x的类别。


学习损失的学习

前面提到,Meta Learning处理的主要是one-shot learning,而one-shot learning需要让学习的速度更快。学习效率除了用更好的梯度来提高,也可以考虑寻找更好的损失函数。根据这种思路,或许可以构造一个模型利用以往的任务来学习如何预测loss。

如上图所示,作者构造了一个Meta-Critic Network,其作用是用来学习如何预测Actor Network的loss。这也是一种思路吧。


学习结构的学习

还有一种思路就是结合深度学习动态地生成一个新的模型。

这种方法能使得模型更加准确,但不一定能够更有效地使用较少的样本来学习,不适合用于one-shot learning。


小结

套娃预警!
在古印度神话中,有人问世界在哪?答案是在乌龟的背上。那么乌龟在哪?乌龟在另一个大乌龟的背上。那么大乌龟在哪…

那么,我们之前让机器learn,现在想办法让机器learn to learn,那之后是不是还要learn to learn to learn再learn to learn to learn to learn呢?哈哈套娃警告。
难以想象未来的机器会是怎么样的,如此智能真的有点细思极恐呢。话说回来,Meta Learning方兴未艾,各种神奇的idea层出不穷,但是真正的杀手级算法尚未出现,让我们拭目以待!

]]>
+

元学习(Meta Learning)即让机器学习如何去学习(learn to learn)。我前几天看到这个概念,觉得非常有意思,这几天也一直在看相关的概念和implement,发现其实很多方法或者思想已经在最新的一些工作中得到有意或者无意运用。因此我决定对我目前的了解做一个总结,或许能给之后的思考带来启发。
强烈推荐台湾教授李宏毅关于Meta Learning的课程录像,油管上有资源,总共4个part,讲得真的非常清楚!
此外,斯坦福在2019年秋季也推出了多任务学习和元学习的CS330,感兴趣可以看一看,语速稍快,可以锻炼一下英语。
由于目前对这个领域的了解还不够深入,分类可能不是很恰当,以后待理解加深之后再做调整,如有错误还请见谅。

References

电子文献:
https://cloud.tencent.com/developer/article/1463397
https://zhuanlan.zhihu.com/p/28639662
https://mp.weixin.qq.com/s/MApZS0-SL4FNP2kmavhcxQ
https://blog.csdn.net/liuglen/article/details/84770069
https://blog.csdn.net/mao_feng/article/details/78939864
https://blog.csdn.net/ppp8300885/article/details/80383246


元学习

人工智能的一个基本问题是它无法像人类一样高效地学习。
与人类相比,大多数最先进的深度学习方法都有两个关键的弱点:

  1. 样本效率低

    深度学习的样本效率很差,学习效率很低下。尽管目前已经有许多深度学习模型显示了超人的表现,但达到这种效果需要数百万个训练样本来训练。也就是说对one-shot learning和few-shot learning的能力还是不够强。
  2. 可移植性差

    许多模型不会从以前的经验或学到的知识中学习。也就是说训练得到的知识是不共享的,并且每个任务都独立于其他任务进行训练。

基于以上这些问题的考虑,“元学习”被提出,它研究的不是如何提升模型解决某项具体的任务(分类、回归、检测)的能力,而是研究如何提升模型解决一系列任务的能力。它是增强学习之后的一个研究分支,总的来看,人工智能的研究呈现出了从Artificial Intelligence,到Machine Learning,到Deep Learning,到Deep Reinforcement Learning,再到Deep Meta Learning这样一个大体的方向,这样下去或许最后就真的能够创造出自己学习的机器了。也就是说:
在过去,我们不仅知道机器是怎么想的,而且知道机器是怎么学的。
目前,我们虽然不知道机器是怎么想的,但是知道机器是怎么学的。
未来,我们非但不知道机器是怎么想的,还不知道机器是怎么学的。
也就是从人工智能到智能。
哈哈上面扯远了,前面说过,我们可以将元学习定义为“学习如何学习”。但是实际上,我们还不知道确切的定义或解决方案。因此,它仍然是一个宽松的术语,指的是不同的方法。比如,从某种意义上来说,我们可以认为迁移学习也是一种元学习。它利用别的任务中训练得比较好的参数,使得在本任务得数据集上能够更好更快地达到最优。
一般可以将Meta Learning分成两个阶段:

  1. Gradual Learning

    也就是通过在一些相似任务上训练得到Meta Learner,它具有较强的泛化能力。
  2. Rapid Learning

    也就是利用该Meta Learner直到之后的学习过程,使模型能够快速调整以适应新的任务。

在李宏毅的课程中提到,Machine Learning其实就是学习如何根据数据找出一个$f$函数,使得在测试中,该函数能根据输入获得正确的输出。而Meta Learning其实就是找到一个$F$函数,这个$F$函数的能力是找出$f$函数,也就是说,不同于机器学习直接找$f$,元学习中机器的任务是去找这个$F$。
来看下图,这里说的其实就是一个训练的过程,通过训练找出最终模型中效果最好的参数$\widehat{\theta }$。

可以看上图红框圈出的部分,仔细想想,这些部分的不同也就构成了不同的算法。然而,目前这些红框中的结构基本上还是人为设计定义的。那么元学习思考的就是能不能让机器自己学习其中的方法与结构。


数据集划分

由于元学习学习的是解决一系列任务的能力,因此不同于以往的将数据集划分成训练集和测试集(或许还有验证集),在元学习中,我们将数据集分割成Training Tasks、Testing Tasks(、Validaiton Tasks)。每一个任务都包含一个训练集和一个测试集。为了区别,这里称这些小训练集为Support Set,称这些小测试集为Query Set。


元学习与one-shot learning

一般来说,元学习总是与one-shot learning相结合的。我们知道,目前许多单任务模型的训练都是以天计数的。而元学习的训练集中每一组数据都是一个任务,假如Training Tasks中有n个任务,那就是说我们要训练n次也就是单任务模型的训练时间的n倍,这也意味着我们需要很长的时间才能检验一次效果,显然是不划算的。
而one-shot learning虽然只提供了少量的数据,但这也是它的一个优势——训练更快,因此我们对one-shot learning任务进行元学习,可以在较短的时间内看到训练效果。


学习初值的元学习

上文说过可以让机器自己学习红框中的结构与方法,这里有元学习中两种代表性的方法MAML和Reptile,它们都学习的是初始化的方式(上图第二个红框)。MAML音同mammal(哺乳动物),也是Model Agnostic Meta Learning的缩写;而Reptile(意为爬行动物),由于找不到缩写的来源,严重怀疑是故意这么命名来与MAML对应。
它们两者实际上学习的是该把初始化参数$\phi $设成什么,其损失函数为

这里的$\widehat{\theta }^{n}$其实就是在第n个任务中,由初始参数$\phi $通过Support Set学到的最终模型中的参数。而$l^{n}\left ( \widehat{\theta }^{n} \right )$就是对于第n个任务在Query Set上的损失。
首先我借用李宏毅老师给的例子来弄明白这里“学习把参数$\phi $初始化成什么”到底与普通的训练中“学习把参数训练成什么”有什么区别。

在上图中,左图表示的是我们一般意义上的训练(Model Pre-training),右图表示的是MAML或者Reptile中的初始化。可以看到,左图最终训练得出的参数$\phi $在task 1(Training Tasks)上表现得最好,但在一个不同的任务task 2(Testing Tasks)上,它只能收敛到局部最优点(local minima)。在右图中,虽然在Training Tasks上表现得不是最好,但是在遇到新的任务训练时,现在的初始化参数$\phi $可以很快很好地收敛到全局最优点(global minima)。
也就是说,一般意义上的训练,我们关注的是在所有Training Tasks上达到最好的情况,比如找出让训练时的损失函数最小的一组参数,但我们并不关注拿此时的参数去新任务上进行训练是否会得到好的结果;而在学习初始化的元学习中,我们不在意$\phi $在Training Tasks中表现得有多好,我们在意的是拿初始化后的$\phi $到下一个任务上去训练能够很快很好地取得不错的表现。简而言之,Model Pre-training关注的是$\phi $当前的表现,而Meta Learning关注的是$\phi $未来遇到新任务时的潜力。如果说到这里还不是很清楚,后文还有直观的理解。
具体怎么找到这个$\phi $呢?这就产生了下面两种方法。

  1. MAML模型无关元学习

    MAML考虑的是训练出一个初始化参数$\phi $,使得拥有这个参数的模型在遇到一个新的任务时能够一步到位达到最好,比如接收到一张新的图片就可以快速训练成为辨别该图片同一类别图片的最优模型。
    当然,寻找这个$\phi $的方法还是梯度下降,因此也要求$\phi $关于损失函数$L$的偏导数。由于MAML考虑的是模型在遇到新任务时一步到位,因此$\widehat{\theta }$也仅进行一次梯度下降,如下所示。 由于这里$\widehat{\theta }_{j}$对$\phi _{i}$的偏导数还是要分类讨论很麻烦,因此作者使用了a first-order approximation,如下图所示。 也就是当且仅当$i$等于$j$的时候时取$\widehat{\theta }_{j}$对$\phi _{i}$的偏导数为1,否则取偏导数为0。
    我们可以来直观地看一下MAML在做什么。如下图所示,左为MAML,右为Model Pre-training,上文已比较过两者关注点的不同,这里再看看两者方法上的不同。 MAML首先在第一个任务采样出来的样本上计算损失函数并进行一次梯度下降,然后用此时的参数计算损失函数再进行一次梯度下降,最后,根据第二次梯度下降的梯度方向,平行地对原参数$\phi $进行一次梯度下降。注意,这里每一次梯度下降使用的学习率不需要一样。可以这样理解,第二次梯度下降后的参数相比第一次梯度下降后的参数在task m上表现要好,因此$\phi $沿相同的方向梯度下降,就能使在遇到task m的时以更少的训练次数到达离较优的第二次梯度下降后的参数更近的位置。在task n上同理。
    而Model Pre-training,则是要求在训练的每个任务上尽可能做到最好,于是直接朝着由每个任务采样样本得到的损失函数计算出的梯度的方向靠近。
    其实,Model Pre-training的基本思想就是每训练完一个任务,我们就把这过程中的信息用来更新模型。而Meta Learning的基本思想就是找到所有学习任务背后的基础知识。因此,我们不会立即更新模型,而是等待一批任务完成。稍后,我们将从这些任务中学到的所有知识合并到一个更新中。 个人感觉,这有点像想要去做、却为了大局而保持一点克制的感觉,似乎的确蕴含了一点点人生哲理在里面。
  2. Reptile一阶元学习

    Reptile的思想其实是和MAML一样的,只不过它使用了多次梯度下降后的方向来更新$\phi _{i}$,也就是对遇到一个新任务时的更新次数不加限制,不指望一步到位。 我们可以通过Pre-training、MAML和Reptile的对比图来理解一下Reptile改进的思想,它相当于在当下的任务和未来的任务之间寻找一个平衡。 我觉得就泛化能力而言,MAML可能还是要好于Reptile的。
    实际上,在MAML的使用中,我们也可以进行多次梯度下降,训练时假设一步到位并不意味着在实际使用的时候只能使用一次梯度下降。

李宏毅老师在课程中还给出了一个toy example的例子,区分Model Pre-training和Meta Learning。如下所示,toy example的目标是拟合正弦曲线

这里的$a$和$b$是任取的随机数,也就是每个任务都是拟合一个不同的正弦曲线。
来看看两种方法的训练结果,其中左边是Model Pre-training的结果,右边是Meta Learning的结果,这里使用的是MAML方法。

图中绿色的线就是训练好之后的参数构成的模型。可以看到,Model Pre-training为了在训练集中各种正弦曲线中达到损失函数最小,训练结果为一条直线,这在测试时效果很差;而MAML则预备在遇到一个新的曲线时能很快地去拟合,在测试时能很快地达到很好的效果。
然而,目前这类方法的效果好像仅在多样性较低的任务分布中进行实验得到了验证,在这些任务上做得好不代表在复杂程度不同、模式不同的任务上也可以有很好的表现。对于应用在高复杂度的任务上的效果可能还是要打个问号的,在这方面的研究还有很长的路要走。

补充:其实这两种方法后面还是有比较复杂的数学推导的,我在网上看到一个博客写得挺好,补充在这里MAML-模型无关元学习算法Reptile-一阶元学习算法


学习更新的学习

上一节介绍的是如何让机器自己学习出一个初始化的方法,前文圈出的红框中另有一项是Update,那么我们是否可以学习如何更新模型呢?

借鉴LSTM

之前没有看过LSTM,所以我想顺便也理一下LSTM的基本思路。

相比于RNN,LSTM从前一级获得的输入有两个(也可以看作将一个输入拆成两段),区别就在于$c$更新得更慢,$h$更新得更快。

这里各个参数表示的含义如下所示,$\sigma $表示的是某种函数。

每一块的输出和传递给下一级的$c$与$h$计算方法如下所示,这里表示的是点乘操作。

  1. 方法1

    一般而言,我们梯度下降使用的是如下公式:对比LSTM中$c$的的更新公式$c^{t}=z^{f}\odot c^{t-1}+z^{i}\odot z$,我们很容易发现两者的结构有相通之处,由此产生灵感,我们是否可以把这里的$z^{f}$和$z^{i}$也变成可以学习的呢。也就是输入$\theta ^{t-1}$和$-\bigtriangledown _{\theta }l$,不人为设定$z^{f}$和$z^{i}$,直接输出$\theta ^{t}$。
    于是就有了如下方法。 但是,由于$-\bigtriangledown _{\theta }l$的计算依赖于$\theta ^{t-1}$(上图虚线表示),这在反向传播时会比较复杂,因此为了简化,在反向传播时不考虑$-\bigtriangledown _{\theta }l$对$\theta ^{t-1}$的依赖。 注意,如果对所有的参数都设计这样一种学习如何更新的方式,那么计算量是非常大的,因此我们往往对所有参数采用同一套学习出来的更新方式。这是合理的,因为往往我们也会对所有参数设置同样的学习率。
    作者对此进行实验来看一看$z^{f}$和$z^{i}$的学习结果究竟如何。如下所示,可见$z^{f}$最后还是收敛到了1,而$z^{i}$也集中在1附近,但也的确学到了一些不一样的信息。 该实验可以表明,我们一般使用的梯度下降公式是比较合理的。不过这里元学习的方法并不是没有作用,后面会提到它的效果。
  2. 方法2

    为了利用更多以往的“记忆”,也可以设计一种两层的LSTM,如下图所示。 实际上,LSTM这种循环序列模型本身就有利用过往记忆的功能,我将在下一节做更多的介绍。
  3. 效果

    其实这些方法还是比较难实现的,这里来看一下利用LSTM来更新参数最后的效果。 可以看到,这种方法在各种优化算法面前还是有明显的优势的。这里有一个问题是作者进行的实验都是在MNIST数据集上进行的,比如说这里Meta Learning的Training Tasks是识别0,1,2,3…7,Testing Tasks是识别8,9。其实这些任务是同一类任务。作者也注意到了这一点,因此他在训练时用的是20units,测试时使用了40units;训练时只用了1个layer,测试时用了2个layer。以此来证明利用LSTM来更新参数这种方法在不同的模型中的能力。

    预测梯度

    既然Meta Learning的目的是实现快速学习,而快速学习的关键就是神经网络的梯度下降要准且快,那么是否可以让神经网络利用以往的任务学习如何预测梯度,这样面对新的任务,只要梯度预测得准,那么学习得就会更快。
    在Learning to Learn by Gradient Descent by Gradient Descent一文中(好好品味这个题目),作者提出了这样一种方法,即训练一个通用的神经网络来预测梯度,用一次二次方程的回归问题来训练,这种方法得到的神经网络优化算法比Adam、RMSProp还要好。

学习记忆的学习

元学习其实也可以利用以往的经验来学习,于是这又诞生了一系列模型。

记忆增强神经网络

一般的神经网络不具有记忆功能,输出的结果只基于当前的输入。而LSTM网络的出现则让网络有了记忆,它能够根据之前的输入给出当前的输出。但是,LSTM的记忆程度并不是那么理想,对于比较长的输入序列,LSTM的最终输出只与最后的几步输入有关,也就是long dependency问题,当然这个问题可以由注意力机制解决,然而还是不能从根本上解决长期记忆的问题,原因是由于LSTM是假设在时间序列上的输入输出:由t-1时刻得到t时刻的输出,然后再循环输入t时刻的结果得到t+1时刻的输出,这样势必会使处于前面序列的输入被淹没,导致这部分记忆被“丢掉”。
基于以上问题,研究者们提出了神经图灵机等记忆增强神经网络模型。我们知道,人除了用脑子记,还会使用做笔记等方式做辅助,当我们忘记时,我们可以去看笔记来获得相关的记忆。记忆增强神经网络的思想就是如此,它让所有的笔记的“读”“写”操作都可微分化,因此可以用神经网络误差后向传播的方式去训练模型。
在记忆增强神经网络中,我们引入外部存储器来存储样本表示和类标签信息。以Meta-Learning with Memory-augmented Neural Networks这篇文章为例,我们看一下它的网络结构。

我们可以看到,网络的输入把上一次的y label也作为输入,并且添加了external memory存储上一次的x输入,这使得下一次输入后进行反向传播时,可以让y label和x建立联系,使得之后的x能够通过外部记忆获取相关图像进行比对来实现更好的预测。

WaveNet

WaveNet的网络每次都利用了之前的数据,因此可以考虑WaveNet的方式来实现Meta Learning。

SNAIL

SNAIL意为蜗牛(为什么这么多动物名…),其实直接把论文标题缩写之后和SNAIL是不一样的,之所以命名成SNAIL可能是因为网络结构有点像蜗牛吧。

  1. 时序卷积

    时序卷积是通过在时间维度上膨胀的一维卷积生成时序数据的结构,如下图所示。 这种时序卷积是因果的,所以在下一个时间节点生成的值只会被之前时间节点的信息影响,而不受未来信息的影响。相比较于传统的RNN,它提供了一种更直接,更高带宽的方式来获取过去的信息。但是,为了处理更长的序列,膨胀率通常是指数级增长的,所以需要的卷积层数和序列长度呈对数关系。因此,只能对很久之前的信息进行粗略的访问,有限的容量和位置依赖性对于元学习方法是不利的,不能充分利用大量的先前的经验。
  2. 注意力模块

    注意力模块可以让模型在可能的无限大的上下文中精确的定位信息,把上下文信息当做无序的键值对,通过内容对其进行查找。
  3. 网络

    SNAIL由两个时序卷积和attention交错组成。时序卷积可以在有限的上下文中提供高带宽的访问方式,attention可以在很大的上下文中精确地访问信息,所以将二者结合起来就得到了SNAIL。在时序卷积产生的上下文中应用causal attention,可以使网络学习应该挑选聚集哪些信息,以及如何更好地表示这些信息。

    注意:英语小贴士,因果:causal;休闲:casual。


学习方法的学习

元学习还用于对数据处理中的一些方法进行学习,比如这里要介绍的对特征提取方法的学习。

Siamese Network孪生网络

Siamese Network中文译为孪生网络。常用于人脸验证(不同于人脸识别),近几年在目标跟踪领域也大放光彩。
在Siamese Network中,我们首先使用两个共享模型参数值的相同网络来提取两个样本的特征(也有个别伪Siamese Network,它们不共享网络权重)。然后,我们将提取出的特征输入鉴别器,判断两个样本是否属于同一类对象。例如,我们可以计算其特征向量的余弦相似度(p)。如果它们相似,那么p应该接近1;否则,它们应该接近0。根据样本的标签和p,我们对网络进行相应的训练。简而言之,我们希望找到使样例属于同一类或将它们区分开来的特性。

这里利用神经网络的好处是它能够学习到哪些特征是该提取的,比如在一些传统的方法中,它们会对整个图像进行特征转化,其中也将包括背景,这样就可能会导致当两个相似背景但不同对象的图片作为输入时,输出会判定为相同。而神经网络它可以在训练过程中学习到哪些部分是值得关注的,可以学会ignore背景。
李宏毅老师在讲这一块的时候还提到了一种思路,就是学习一种算法(生成网络),这种算法能够使用one-shot learning输入的少量样本生成许多样本用于和prediction的那个输入进行比对验证,其框架与Siamese Network基本相似。

Matching Network匹配网络

Matching Network与Siamese Network十分相似。我们知道,人的注意力是可以利用以往的经验来实现提升的。那么,能不能利用以往的任务来训练一个attention模型,使得模型面对新的任务时,能够直接关注最重要的部分。
于是就有了Matching Network。

这里的g和f是特征提取器,和Siamese Network一样使用深度网络来提取特征,用于我们的输入和测试样本。通常情况下,g和f是相同的,共享相同的深度网络。然后,我们比较它们的相似度。同样,我们从预测中计算一个损失函数来训练我们的特征提取器。
这里构造了一个attention机制,也就是最后的label判断是通过attention的叠加得到的:

其实这里的网络结构远比上面画出来的复杂(用到了LSTM)。我们可以来看一下它的表现,在Omniglot数据集(找一批人照着象形文字画出来的)上,它取得了比Siamese Network更好一点的成绩。不过具体能力如何,还得看论文跑实验。

Prototypical Network原型网络

该方法思想十分简单,效果也非常好。它学习的是一个度量空间,通过计算和每个类别的原型表达的距离来进行分类。
具体的思路是:每个类别都存在一个聚在某单个原型表达周围的embedding,该类的原型是Support Set在embedding空间中的均值。然后,分类问题变成在embedding空间中的最近邻。如图所示,c1、c2、c3分别是三个类别的均值中心(称Prototype),将测试样本x进行embedding后,与这3个中心进行距离计算,从而获得x的类别。


学习损失的学习

前面提到,Meta Learning处理的主要是one-shot learning,而one-shot learning需要让学习的速度更快。学习效率除了用更好的梯度来提高,也可以考虑寻找更好的损失函数。根据这种思路,或许可以构造一个模型利用以往的任务来学习如何预测loss。

如上图所示,作者构造了一个Meta-Critic Network,其作用是用来学习如何预测Actor Network的loss。这也是一种思路吧。


学习结构的学习

还有一种思路就是结合深度学习动态地生成一个新的模型。

这种方法能使得模型更加准确,但不一定能够更有效地使用较少的样本来学习,不适合用于one-shot learning。


小结

套娃预警!
在古印度神话中,有人问世界在哪?答案是在乌龟的背上。那么乌龟在哪?乌龟在另一个大乌龟的背上。那么大乌龟在哪…

那么,我们之前让机器learn,现在想办法让机器learn to learn,那之后是不是还要learn to learn to learn再learn to learn to learn to learn呢?哈哈套娃警告。
难以想象未来的机器会是怎么样的,如此智能真的有点细思极恐呢。话说回来,Meta Learning方兴未艾,各种神奇的idea层出不穷,但是真正的杀手级算法尚未出现,让我们拭目以待!

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>元学习(Meta Learning)即让机器学习如何去学习(learn to learn)。我前几天看到这个概念,觉得非常有意思,这几天也一直在 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>元学习(Meta Learning)即让机器学习如何去学习(learn to learn)。我前几天看到这个概念,觉得非常有意思,这几天也一直在 @@ -493,13 +493,13 @@ 2020-02-02T05:32:16.000Z 2020-02-07T04:27:33.180Z -

总是看到对图像进行上采样、对图像进行下采样,感觉好像懂了又不知道具体作了什么,这里就来搞搞懂。
本文针对的是图像处理中的上采样和下采样。

References

电子文献:
https://www.cnblogs.com/tectal/p/10138432.html
https://buptldy.github.io/2016/10/29/2016-10-29-deconv/


下采样

下采样即缩小图像,主要有两个目的:使得图像符合需要的大小;生成对应图像的缩略图。
下采样的原理很简单,比如对于一幅尺寸为MxN的图像,对其进行s倍下采样,即得到(M/s)x(N/s)尺寸的图像。这可以通过把原始图像划成sxs的窗口,使每个窗口内的图像变成一个像素,这个像素点的值可以是窗口内所有像素的均值或者最大值等等。另外我认为高斯滤波等卷积方式本身也是一种下采样。


上采样

上采样与下采样相反,其目的是放大图像。注意,它并不能带来更多关于该图像的信息,因此图像的质量会受到影响。
上采样的实现主要有两种思路。一种是内插值的方法,另一种是采用反卷积(也称转置卷积)的方法。

注:还有一种现在已经比较少用的方法也就是反池化,想了解的话可以看一下computer-vision笔记:反池化


插值方法

  1. 最邻近元法

    这种方法最简单,不需要计算,即在待求像素的四个邻像素中,选取距离待求像素最近的邻像素的灰度值赋给待求像素。 如上图所示,新增在A区内的像素就用左上角的像素点来赋值,其余三个区域同理。
    虽然最邻近元法计算量较小,但可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能会出现明显的锯齿状。
  2. 双线性插值法

    这种方法也很好理解,就是把原本四个像素点围成的新的区域中的灰度值变化视作线性的,然后根据插入点的位置,按照比例计算两次,得到最终要赋的值。 这也是一种比较常用的插值方法。
  3. 三次内插法

    该方法是理论上最优的sinc函数(辛格函数)逼近,其数学表达式为:$Sinc\left ( x \right )=\frac{sin\left ( x \right )}{x}$
    待求像素的灰度值由其周围16个灰度值加权内插得到,如下图所示。 待求像素的灰度计算式如下:其中$ABC$的含义分别如下所示。 三次内插法计算量比较大,但插值后的图像效果最好。

反卷积

反卷积通常用于如下几个方面:在CNN可视化处理中,可以通过反卷积将卷积得到的feature map还原到像素空间,来观察feature map对哪些pattern相应最大,即可视化哪些特征是卷积操作提取出来的;在FCN全卷积网络中,由于要对图像进行像素级的分割,需要将图像尺寸还原到原来的大小,类似upsampling的操作,所以需要采用反卷积;在GAN对抗式生成网络中,由于需要从输入图像到生成图像,自然需要将提取的特征图还原到和原图同样尺寸的大小,即也需要反卷积操作。
为了方便理解和比较,我们将卷积和反卷积对照着看。
我们先来看卷积时使用3x3卷积核,且没有padding,strides为1时的情况。


可以看到,由于卷积使用了no padding,那么对应的反卷积就要添加padding,反卷积的padding值可用padding = kernel_size - stride来计算。在上面的情况中,反卷积的padding就是3减1等于2。
前面说到反卷积也称转置卷积,这里说一下原因。
对于上面3x3卷积核的计算,我们可以通过这样一个4x16的稀疏矩阵来把卷积变成矩阵相乘$Y=CX$。

注意到这里每一行有9个数值和7个0,也就是说在上面4x4的输入特征中,对每一个输出特征,有9个输入特征参与了计算。
如此,卷积的前向操作可以表示为输入和稀疏矩阵$C$相乘。类似的,卷积的反向传播就是输出和$C$的转置相乘。

注意:熟悉线性代数的读者应该会发现,要实现上面的运算,不应该是求矩阵的逆吗。信息论告诉我们,卷积是不可逆的,因此要注意,上面的两个$C$指的并不是同一个,即用来进行反卷积的权重矩阵不一定来自于原卷积矩阵,但该权重矩阵的形状和转置后的原卷积矩阵完全相同。
此外,在一些深度学习的开源框架中,并不是通过这种转换方法来计算卷积的,因为这个转换会存在很多无用的0乘操作。

还有一个要注意的是,当卷积的stride为2时,要对输入矩阵中间补零才能完成反卷积,其实相当于进行了两次padding,一次内插补零,一次在外圈补零。

]]>
+

总是看到对图像进行上采样、对图像进行下采样,感觉好像懂了又不知道具体作了什么,这里就来搞搞懂。
本文针对的是图像处理中的上采样和下采样。

References

电子文献:
https://www.cnblogs.com/tectal/p/10138432.html
https://buptldy.github.io/2016/10/29/2016-10-29-deconv/


下采样

下采样即缩小图像,主要有两个目的:使得图像符合需要的大小;生成对应图像的缩略图。
下采样的原理很简单,比如对于一幅尺寸为MxN的图像,对其进行s倍下采样,即得到(M/s)x(N/s)尺寸的图像。这可以通过把原始图像划成sxs的窗口,使每个窗口内的图像变成一个像素,这个像素点的值可以是窗口内所有像素的均值或者最大值等等。另外我认为高斯滤波等卷积方式本身也是一种下采样。


上采样

上采样与下采样相反,其目的是放大图像。注意,它并不能带来更多关于该图像的信息,因此图像的质量会受到影响。
上采样的实现主要有两种思路。一种是内插值的方法,另一种是采用反卷积(也称转置卷积)的方法。

注:还有一种现在已经比较少用的方法也就是反池化,想了解的话可以看一下computer-vision笔记:反池化


插值方法

  1. 最邻近元法

    这种方法最简单,不需要计算,即在待求像素的四个邻像素中,选取距离待求像素最近的邻像素的灰度值赋给待求像素。 如上图所示,新增在A区内的像素就用左上角的像素点来赋值,其余三个区域同理。
    虽然最邻近元法计算量较小,但可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能会出现明显的锯齿状。
  2. 双线性插值法

    这种方法也很好理解,就是把原本四个像素点围成的新的区域中的灰度值变化视作线性的,然后根据插入点的位置,按照比例计算两次,得到最终要赋的值。 这也是一种比较常用的插值方法。
  3. 三次内插法

    该方法是理论上最优的sinc函数(辛格函数)逼近,其数学表达式为:$Sinc\left ( x \right )=\frac{sin\left ( x \right )}{x}$
    待求像素的灰度值由其周围16个灰度值加权内插得到,如下图所示。 待求像素的灰度计算式如下:其中$ABC$的含义分别如下所示。 三次内插法计算量比较大,但插值后的图像效果最好。

反卷积

反卷积通常用于如下几个方面:在CNN可视化处理中,可以通过反卷积将卷积得到的feature map还原到像素空间,来观察feature map对哪些pattern相应最大,即可视化哪些特征是卷积操作提取出来的;在FCN全卷积网络中,由于要对图像进行像素级的分割,需要将图像尺寸还原到原来的大小,类似upsampling的操作,所以需要采用反卷积;在GAN对抗式生成网络中,由于需要从输入图像到生成图像,自然需要将提取的特征图还原到和原图同样尺寸的大小,即也需要反卷积操作。
为了方便理解和比较,我们将卷积和反卷积对照着看。
我们先来看卷积时使用3x3卷积核,且没有padding,strides为1时的情况。


可以看到,由于卷积使用了no padding,那么对应的反卷积就要添加padding,反卷积的padding值可用padding = kernel_size - stride来计算。在上面的情况中,反卷积的padding就是3减1等于2。
前面说到反卷积也称转置卷积,这里说一下原因。
对于上面3x3卷积核的计算,我们可以通过这样一个4x16的稀疏矩阵来把卷积变成矩阵相乘$Y=CX$。

注意到这里每一行有9个数值和7个0,也就是说在上面4x4的输入特征中,对每一个输出特征,有9个输入特征参与了计算。
如此,卷积的前向操作可以表示为输入和稀疏矩阵$C$相乘。类似的,卷积的反向传播就是输出和$C$的转置相乘。

注意:熟悉线性代数的读者应该会发现,要实现上面的运算,不应该是求矩阵的逆吗。信息论告诉我们,卷积是不可逆的,因此要注意,上面的两个$C$指的并不是同一个,即用来进行反卷积的权重矩阵不一定来自于原卷积矩阵,但该权重矩阵的形状和转置后的原卷积矩阵完全相同。
此外,在一些深度学习的开源框架中,并不是通过这种转换方法来计算卷积的,因为这个转换会存在很多无用的0乘操作。

还有一个要注意的是,当卷积的stride为2时,要对输入矩阵中间补零才能完成反卷积,其实相当于进行了两次padding,一次内插补零,一次在外圈补零。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>总是看到对图像进行上采样、对图像进行下采样,感觉好像懂了又不知道具体作了什么,这里就来搞搞懂。<br>本文针对的是图像处理中的上采样和下采样。< + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>总是看到对图像进行上采样、对图像进行下采样,感觉好像懂了又不知道具体作了什么,这里就来搞搞懂。<br>本文针对的是图像处理中的上采样和下采样。< @@ -519,13 +519,13 @@ 2020-02-01T03:34:16.000Z 2020-02-15T14:08:32.140Z -

最近看了目标检测中比较经典的SSD,觉得有不少挺好的创新。之前的文章中也提到过,由于one-stage的检测方法和two-stage的检测方法都存在着速度与精度平衡的问题,所以SSD在借鉴YOLO的网络架构和Faster RCNN中的anchor box实现多尺度的思想的基础上,设计出了效果更好的算法。而DSSD是SSD众多改进版本中比较突出的一支,主要是牺牲了fps来换取精度。本篇文章我就不花时间去写两个模型的流程了,主要是记录一些我觉得比较好的点。

References

电子文献:
https://zhuanlan.zhihu.com/p/33544892
https://www.cnblogs.com/edbean/p/11335139.html
https://www.zhihu.com/question/58200555


SSD和DSSD

背景

在SSD之前,目标检测主要有两种思路,一种是以YOLO为代表的基于一体化卷积网络的检测,即使用端到端的one-stage检测方法,其主要思路是均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,然后利用CNN提取特征后直接进行分类与回归,整个过程只需要一步,可参考deep-learning笔记:端到端学习;另一种思路是以RCNN系列为代表的先提取候选区域再进行分类与回归的two-stage检测方法,详见computer-vision笔记:RPN与Faster RCNN。前者的优势在速度,但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡,导致模型准确度稍低;后者的优势在精度,但总是无法取得平衡且更好的效果。

SSD

本文要介绍的SSD算法,其英文全名是Single Shot MultiBox Detector,其中Single shot指明了SSD算法属于one-stage方法,MultiBox指明了SSD使用了多框预测。

上图是几种方法的基本框架对比图,对于Faster RCNN,其先通过CNN得到候选框,然后再进行分类与回归,而YOLO与SSD可以一步到位完成检测。
相比YOLO,SSD采用CNN来直接进行检测,而不是像YOLO那样在全连接层之后做检测,这是SSD相比YOLO的其中一个不同点。另外还有两个重要的改变:一是SSD提取了不同尺度的特征图来做检测,大尺度特征图(较靠前的特征图)可以用来检测小物体,而小尺度特征图(较靠后的特征图)用来检测大物体(见下图);二是SSD采用了不同尺度和长宽比的先验框(Prior boxes,Default boxes,在Faster RCNN中叫做锚,Anchors)。YOLO算法缺点是难以检测小目标,而且定位不准,而上面这几点重要的改进使得SSD在一定程度上克服这些缺点。

DSSD

DSSD其实就是D加SSD。其中D代表反卷积,其重要意义就是可以提升分辨率,而提升分辨率的重要效果就是小物体检测性能提升;SSD代表其使用的backbone,这部分的与SSD的结构上无较大差异,主要是将VGG换为了更深的ResNet-101来获得更高的准确度,并在其后加入数个卷积层。单纯加卷积层并不能直接提升精度,但是在这之后加入后文提到的prediction module后就能极大地提升精度。
由于利用single-scale输出预测multi-scale物体在精度方面具有一定劣势,因此SSD利用各层的feature map的输出感受野各不相同的特性,用大的感受野检测大型物体,用小的感受野检测小型物体。但是若直接利用浅层的输出检测小型物体,由于浅层网络提取的语义信息较少(含有较多的背景和噪声),不够鲁棒,所以最终表现还不是很好。
这里我想介绍一下稍早于DSSD的FPN,它先还是一如既往地使用bottom-up的深度网络,然后又构建top-down网络进行上采样操作,这不仅使得它可以利用经过top-down模型后的那些上下文信息即高层语义信息,也可以在更大的feature map上面进行操作,提高了分辨率,可以获得更多关于小目标的有用信息。此外,不同于许多算法(SSD等)采用多尺度特征融合之后再做预测的方式,FPN的预测是在不同的特征层独立进行的。
于是DSSD从中得到启发,利用FPN的思想来改进SSD,它使用反卷积和skip connection扩大图像,这样除了能提升分辨率,还能保证语义信息充足。关于反卷积的实现,可参考computer-vision笔记:上采样和下采样
这样就形成了一个“宽-窄-宽”沙漏型的结构,其中网络的中间层用于编码(encode)输入图像的信息,然后再逐渐用更大的层来自上而下地解码(decode)在这整个图像上的图。要注意的是,这里的反卷积并不能复原原来的图像。类似的思想可以看一下我整理的computer-vision笔记:图像金字塔与高斯滤波器


空洞卷积(atrous convolutions)

在SSD中,作者采用了一种名为空洞卷积(atrous convolutions,又名扩张卷积(dilated convolutions))的卷积方式,向卷积层引入一个称为“扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时选取各值之间的间距。

空洞卷积的有效性基于一个假设,即紧密相邻的像素几乎相同,全部纳入会产生冗余,不如每隔H(hole size)个选取一个。
在相同的计算条件下,空洞卷积提供了更大的感受野。它经常用在实时图像分割中。当网络层需要较大的感受野,但计算资源有限而无法提高卷积核数量或大小时,可以考虑使用空洞卷积。

值得注意的是,空洞卷积在增大感受野的情况下也维持了分辨率(也就是说在相同感受野下分辨率更高),这在语义分割等问题中是比较实用的。此外,用步长为2的卷积操作代替池化也可以有效地减少分辨率的损失。


难例挖掘(hard negative mining)

首先介绍一下什么是难例。
根据IoU我们可以把采样获得的正负样本分成难易样本。
简单负样本:与GT没有任何交集。这种样本是最多的,在图像中随意采集往往都属于这一类。
简单正样本:与GT的交集远远大于阈值。也就是非常接近正确结果的。
困难负样本:与GT有交集,但小于阈值。
困难正样本:与GT有交集,且仅略大于阈值。
后面两种就是我们所说的难例,其loss较大。若在采样时难例占比太小,那么总体的loss就不大,这对训练来说是不利的。

因为本身正样本比较稀少,因此难例挖掘主要关注的是困难负样本,其基本策略是:

  1. 选取所有的正样本,数量记为k个。
  2. 对所有的负样本求loss,递减排序,选取前3k个。
  3. 用这k个正样本,3k个负样本参与损失计算与反向传播。(SSD和DSSD都采取了这样的比例)

smoothL1

在使用损失函数时,我们以往一般使用的是L1损失或者L2损失,而在Faster RCNN和SSD中都使用了smoothL1来作为损失函数。

为了更清楚地探究smoothL1的作用,我们将三者对x求导。

可以看到,对L1,其损失函数的导数随着x的增大而增大,这就导致了在训练初期,预测值与ground truth差异过于大时,损失函数对预测值的梯度十分大,训练不稳定。
对L2,其损失函数的导数为常数,这就导致了在训练后期,预测值与ground truth差异已经很小时,损失函数的导数的绝对值仍然为1,此时如果learning rate不变,那么模型参数将在稳定值附近波动,难以继续收敛以达到更高精度。一种思路是可以让学习率动态衰减,方法及实现详见deep-learning笔记:学习率衰减与批归一化
而smoothL1则从两个方面限制了梯度:
(1)当预测框与ground truth差别过大时,梯度值不至于过大。也就是避免了梯度爆炸,使得模型更加健壮。
(2)当预测框与ground truth差别很小时,梯度值足够小。
可以说smoothL1很好地解决了L1和L2的缺陷,且比较简便。


残差模块

在预测模块中,DSSD也对SSD进行了改进,在feature map之后还引入了残差模块来提升性能,如下图所示。

经比较可以发现,引入1个残差单元(即上方图c)的效果最好。


移去批归一化

在DSSD中,为了提高测试速度,作者还通过去掉BN层来提高模型的速度。
根据论文中的一组公式,我们可以简单地了解一下作者的方法。

实际上,作者的方法就是把批归一化进行变换拆分,从而将去掉的BN层加入到卷积层中。具体就是rewrite一个卷积层中如等式2所示的weight和如等式3所示的bias,从而就不需要等式4中相关的批归一化变量了。
作者通过实验也证明了这种方法对速度的提升(尽管还是很低)。


网络训练

因为在Fast RCNN和Faster RCNN中没有特征或者像素重新采样阶段,所以它们依赖于数据增强(data augmentation)。主要的方法有随机剪裁(random cropping),光度失真(random photometric distortion)和随机翻转(random flipping)。
在SSD中还包括了一种随机扩展(random expansion)的数据增强trick,这种trick对小检测对象比较有用。
DSSD在SSD的基础上进行训练,即先引入预训练得比较好的SSD,然后冻结SSD部分训练后面的部分,最后以较小的学习率训练整个网络来进行微调(fine-tuning)。

]]>
+

最近看了目标检测中比较经典的SSD,觉得有不少挺好的创新。之前的文章中也提到过,由于one-stage的检测方法和two-stage的检测方法都存在着速度与精度平衡的问题,所以SSD在借鉴YOLO的网络架构和Faster RCNN中的anchor box实现多尺度的思想的基础上,设计出了效果更好的算法。而DSSD是SSD众多改进版本中比较突出的一支,主要是牺牲了fps来换取精度。本篇文章我就不花时间去写两个模型的流程了,主要是记录一些我觉得比较好的点。

References

电子文献:
https://zhuanlan.zhihu.com/p/33544892
https://www.cnblogs.com/edbean/p/11335139.html
https://www.zhihu.com/question/58200555


SSD和DSSD

背景

在SSD之前,目标检测主要有两种思路,一种是以YOLO为代表的基于一体化卷积网络的检测,即使用端到端的one-stage检测方法,其主要思路是均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,然后利用CNN提取特征后直接进行分类与回归,整个过程只需要一步,可参考deep-learning笔记:端到端学习;另一种思路是以RCNN系列为代表的先提取候选区域再进行分类与回归的two-stage检测方法,详见computer-vision笔记:RPN与Faster RCNN。前者的优势在速度,但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡,导致模型准确度稍低;后者的优势在精度,但总是无法取得平衡且更好的效果。

SSD

本文要介绍的SSD算法,其英文全名是Single Shot MultiBox Detector,其中Single shot指明了SSD算法属于one-stage方法,MultiBox指明了SSD使用了多框预测。

上图是几种方法的基本框架对比图,对于Faster RCNN,其先通过CNN得到候选框,然后再进行分类与回归,而YOLO与SSD可以一步到位完成检测。
相比YOLO,SSD采用CNN来直接进行检测,而不是像YOLO那样在全连接层之后做检测,这是SSD相比YOLO的其中一个不同点。另外还有两个重要的改变:一是SSD提取了不同尺度的特征图来做检测,大尺度特征图(较靠前的特征图)可以用来检测小物体,而小尺度特征图(较靠后的特征图)用来检测大物体(见下图);二是SSD采用了不同尺度和长宽比的先验框(Prior boxes,Default boxes,在Faster RCNN中叫做锚,Anchors)。YOLO算法缺点是难以检测小目标,而且定位不准,而上面这几点重要的改进使得SSD在一定程度上克服这些缺点。

DSSD

DSSD其实就是D加SSD。其中D代表反卷积,其重要意义就是可以提升分辨率,而提升分辨率的重要效果就是小物体检测性能提升;SSD代表其使用的backbone,这部分的与SSD的结构上无较大差异,主要是将VGG换为了更深的ResNet-101来获得更高的准确度,并在其后加入数个卷积层。单纯加卷积层并不能直接提升精度,但是在这之后加入后文提到的prediction module后就能极大地提升精度。
由于利用single-scale输出预测multi-scale物体在精度方面具有一定劣势,因此SSD利用各层的feature map的输出感受野各不相同的特性,用大的感受野检测大型物体,用小的感受野检测小型物体。但是若直接利用浅层的输出检测小型物体,由于浅层网络提取的语义信息较少(含有较多的背景和噪声),不够鲁棒,所以最终表现还不是很好。
这里我想介绍一下稍早于DSSD的FPN,它先还是一如既往地使用bottom-up的深度网络,然后又构建top-down网络进行上采样操作,这不仅使得它可以利用经过top-down模型后的那些上下文信息即高层语义信息,也可以在更大的feature map上面进行操作,提高了分辨率,可以获得更多关于小目标的有用信息。此外,不同于许多算法(SSD等)采用多尺度特征融合之后再做预测的方式,FPN的预测是在不同的特征层独立进行的。
于是DSSD从中得到启发,利用FPN的思想来改进SSD,它使用反卷积和skip connection扩大图像,这样除了能提升分辨率,还能保证语义信息充足。关于反卷积的实现,可参考computer-vision笔记:上采样和下采样
这样就形成了一个“宽-窄-宽”沙漏型的结构,其中网络的中间层用于编码(encode)输入图像的信息,然后再逐渐用更大的层来自上而下地解码(decode)在这整个图像上的图。要注意的是,这里的反卷积并不能复原原来的图像。类似的思想可以看一下我整理的computer-vision笔记:图像金字塔与高斯滤波器


空洞卷积(atrous convolutions)

在SSD中,作者采用了一种名为空洞卷积(atrous convolutions,又名扩张卷积(dilated convolutions))的卷积方式,向卷积层引入一个称为“扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时选取各值之间的间距。

空洞卷积的有效性基于一个假设,即紧密相邻的像素几乎相同,全部纳入会产生冗余,不如每隔H(hole size)个选取一个。
在相同的计算条件下,空洞卷积提供了更大的感受野。它经常用在实时图像分割中。当网络层需要较大的感受野,但计算资源有限而无法提高卷积核数量或大小时,可以考虑使用空洞卷积。

值得注意的是,空洞卷积在增大感受野的情况下也维持了分辨率(也就是说在相同感受野下分辨率更高),这在语义分割等问题中是比较实用的。此外,用步长为2的卷积操作代替池化也可以有效地减少分辨率的损失。


难例挖掘(hard negative mining)

首先介绍一下什么是难例。
根据IoU我们可以把采样获得的正负样本分成难易样本。
简单负样本:与GT没有任何交集。这种样本是最多的,在图像中随意采集往往都属于这一类。
简单正样本:与GT的交集远远大于阈值。也就是非常接近正确结果的。
困难负样本:与GT有交集,但小于阈值。
困难正样本:与GT有交集,且仅略大于阈值。
后面两种就是我们所说的难例,其loss较大。若在采样时难例占比太小,那么总体的loss就不大,这对训练来说是不利的。

因为本身正样本比较稀少,因此难例挖掘主要关注的是困难负样本,其基本策略是:

  1. 选取所有的正样本,数量记为k个。
  2. 对所有的负样本求loss,递减排序,选取前3k个。
  3. 用这k个正样本,3k个负样本参与损失计算与反向传播。(SSD和DSSD都采取了这样的比例)

smoothL1

在使用损失函数时,我们以往一般使用的是L1损失或者L2损失,而在Faster RCNN和SSD中都使用了smoothL1来作为损失函数。

为了更清楚地探究smoothL1的作用,我们将三者对x求导。

可以看到,对L1,其损失函数的导数随着x的增大而增大,这就导致了在训练初期,预测值与ground truth差异过于大时,损失函数对预测值的梯度十分大,训练不稳定。
对L2,其损失函数的导数为常数,这就导致了在训练后期,预测值与ground truth差异已经很小时,损失函数的导数的绝对值仍然为1,此时如果learning rate不变,那么模型参数将在稳定值附近波动,难以继续收敛以达到更高精度。一种思路是可以让学习率动态衰减,方法及实现详见deep-learning笔记:学习率衰减与批归一化
而smoothL1则从两个方面限制了梯度:
(1)当预测框与ground truth差别过大时,梯度值不至于过大。也就是避免了梯度爆炸,使得模型更加健壮。
(2)当预测框与ground truth差别很小时,梯度值足够小。
可以说smoothL1很好地解决了L1和L2的缺陷,且比较简便。


残差模块

在预测模块中,DSSD也对SSD进行了改进,在feature map之后还引入了残差模块来提升性能,如下图所示。

经比较可以发现,引入1个残差单元(即上方图c)的效果最好。


移去批归一化

在DSSD中,为了提高测试速度,作者还通过去掉BN层来提高模型的速度。
根据论文中的一组公式,我们可以简单地了解一下作者的方法。

实际上,作者的方法就是把批归一化进行变换拆分,从而将去掉的BN层加入到卷积层中。具体就是rewrite一个卷积层中如等式2所示的weight和如等式3所示的bias,从而就不需要等式4中相关的批归一化变量了。
作者通过实验也证明了这种方法对速度的提升(尽管还是很低)。


网络训练

因为在Fast RCNN和Faster RCNN中没有特征或者像素重新采样阶段,所以它们依赖于数据增强(data augmentation)。主要的方法有随机剪裁(random cropping),光度失真(random photometric distortion)和随机翻转(random flipping)。
在SSD中还包括了一种随机扩展(random expansion)的数据增强trick,这种trick对小检测对象比较有用。
DSSD在SSD的基础上进行训练,即先引入预训练得比较好的SSD,然后冻结SSD部分训练后面的部分,最后以较小的学习率训练整个网络来进行微调(fine-tuning)。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>最近看了目标检测中比较经典的SSD,觉得有不少挺好的创新。之前的文章中也提到过,由于one-stage的检测方法和two-stage的检测方法都 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>最近看了目标检测中比较经典的SSD,觉得有不少挺好的创新。之前的文章中也提到过,由于one-stage的检测方法和two-stage的检测方法都 @@ -549,13 +549,13 @@ 2020-01-30T14:44:38.000Z 2020-02-15T14:06:57.165Z -

在许多新的算法里,涉及到bounding box时,往往会转化成回归问题。而做出这种转变的开山之作,据我所知应该是YOLO。关于YOLO,网上有很多很好的解读,我也在之前的文章中多次提到过YOLO的一些要素,这里不一一列举了,也不重复了,可以使用我网站内的搜索功能看一下相关内容。Andrew Ng在卷积神经网络的公开课上好像也特地围绕YOLO的相关内容讲了一周吧,也可以去看看。本文主要是想记录一下YOLO设计损失函数的一点trick和一些理解。
注意,本文讨论的是YOLOv1。


损失函数

我们直接看YOLO的损失函数。

可以看到,这里所有的损失都是利用ground truth和预测作差。熟悉机器学习相关知识的读者应该会知道,一般我们计算分类的损失时,一般会使用交叉熵或者逻辑回归,而YOLO的类别损失(上图最后一项)依旧使用了直接作差的方法,所以我们说,YOLO把整个检测问题作为一个回归问题来处理。
暂时不关注其它损失项,我觉得这里比较巧妙的是第二项也就是宽高误差。善于观察的话肯定会发现,宽高误差在这里所有的损失项中略显突兀,即它在作差之前分别对ground truth和预测值开了根号,这是为什么呢?
实际上,YOLO的一个motivation就是解决密集物体和小物体的检测(虽然还是没能很好的解决)。相比于大的Bbox产生预测误差,作者更加关注小的Bbox产生的误差。而平方误差损失(sum-squared error loss)中对同样的偏移loss值是一样的。因此这里用了一种比较巧妙的办法,就是用长和宽的平方根来代替原值,这时小的Bbox发生偏移时,其横轴上反应的loss比大的Bbox发生偏移时要大。

这为我们提供了一种很好的思路,若我们对大跨度的目标更感兴趣或者说更希望较大的目标预测更精确时,我们不妨也可以使用平方或是立方来增强大尺度时的反应。其实我觉得这里的思想其实和Gamma校正是一致的。


YOLO优点

  1. 速度快且容易优化

    YOLO(You Only Look Once)只需读取一次图像就可以进行端到端的优化,这使得它的速度可以很快。它将检测问题转化为回归问题,可以满足实时性的要求。我手机上的一款APP用的就是YOLO的改进版。
  2. 背景误识别率低

    YOLO对全图进行卷积学习,综合考虑了全图的上下文信息,因此背景的误检测较少。
  3. 泛化能力强

    这也是因为综合考虑了图片全局,因此能够更好地学习数据集的本质表达,从而使得泛化性能更好。

YOLO缺点

  1. 定位精度不够

    这尤其是对于小目标,因为网络较深,使得细粒度特征和小物体特征不明显。因为它是端到端的检测算法(one-stage),没有Faster RCNN那样提取候选框的操作,这也导致了它的精度不高。

    补充:由于one-stage和two-stage检测都存在着速度与精度平衡的问题,所以后来的SSD(也是端到端)在借鉴YOLO的网络架构和Faster RCNN中的anchor box实现多尺度的思想的基础上,设计出了更好的算法。可以看一下computer-vision笔记:SSD和DSSD

  2. 对密集目标的识别存在不足

    这主要是因为一个grid cell只能预测一个物体。如下图所示,当两个bicycle靠得比较近时,只预测出了一个bicycle。

  3. 异常长宽比的目标识别不佳

    因为YOLOv1是最后直接学习框的值,这导致了它对异常长宽比的目标识别不佳。
  4. 对目标个数有限制

    由于YOLOv1最后的输出是固定的7x7,也就是说它最多只能预测49个目标。在我前面APP的截图中可以看到标注了max为100,这说明该版本的YOLO固定输出10x10。
]]>
+

在许多新的算法里,涉及到bounding box时,往往会转化成回归问题。而做出这种转变的开山之作,据我所知应该是YOLO。关于YOLO,网上有很多很好的解读,我也在之前的文章中多次提到过YOLO的一些要素,这里不一一列举了,也不重复了,可以使用我网站内的搜索功能看一下相关内容。Andrew Ng在卷积神经网络的公开课上好像也特地围绕YOLO的相关内容讲了一周吧,也可以去看看。本文主要是想记录一下YOLO设计损失函数的一点trick和一些理解。
注意,本文讨论的是YOLOv1。


损失函数

我们直接看YOLO的损失函数。

可以看到,这里所有的损失都是利用ground truth和预测作差。熟悉机器学习相关知识的读者应该会知道,一般我们计算分类的损失时,一般会使用交叉熵或者逻辑回归,而YOLO的类别损失(上图最后一项)依旧使用了直接作差的方法,所以我们说,YOLO把整个检测问题作为一个回归问题来处理。
暂时不关注其它损失项,我觉得这里比较巧妙的是第二项也就是宽高误差。善于观察的话肯定会发现,宽高误差在这里所有的损失项中略显突兀,即它在作差之前分别对ground truth和预测值开了根号,这是为什么呢?
实际上,YOLO的一个motivation就是解决密集物体和小物体的检测(虽然还是没能很好的解决)。相比于大的Bbox产生预测误差,作者更加关注小的Bbox产生的误差。而平方误差损失(sum-squared error loss)中对同样的偏移loss值是一样的。因此这里用了一种比较巧妙的办法,就是用长和宽的平方根来代替原值,这时小的Bbox发生偏移时,其横轴上反应的loss比大的Bbox发生偏移时要大。

这为我们提供了一种很好的思路,若我们对大跨度的目标更感兴趣或者说更希望较大的目标预测更精确时,我们不妨也可以使用平方或是立方来增强大尺度时的反应。其实我觉得这里的思想其实和Gamma校正是一致的。


YOLO优点

  1. 速度快且容易优化

    YOLO(You Only Look Once)只需读取一次图像就可以进行端到端的优化,这使得它的速度可以很快。它将检测问题转化为回归问题,可以满足实时性的要求。我手机上的一款APP用的就是YOLO的改进版。
  2. 背景误识别率低

    YOLO对全图进行卷积学习,综合考虑了全图的上下文信息,因此背景的误检测较少。
  3. 泛化能力强

    这也是因为综合考虑了图片全局,因此能够更好地学习数据集的本质表达,从而使得泛化性能更好。

YOLO缺点

  1. 定位精度不够

    这尤其是对于小目标,因为网络较深,使得细粒度特征和小物体特征不明显。因为它是端到端的检测算法(one-stage),没有Faster RCNN那样提取候选框的操作,这也导致了它的精度不高。

    补充:由于one-stage和two-stage检测都存在着速度与精度平衡的问题,所以后来的SSD(也是端到端)在借鉴YOLO的网络架构和Faster RCNN中的anchor box实现多尺度的思想的基础上,设计出了更好的算法。可以看一下computer-vision笔记:SSD和DSSD

  2. 对密集目标的识别存在不足

    这主要是因为一个grid cell只能预测一个物体。如下图所示,当两个bicycle靠得比较近时,只预测出了一个bicycle。

  3. 异常长宽比的目标识别不佳

    因为YOLOv1是最后直接学习框的值,这导致了它对异常长宽比的目标识别不佳。
  4. 对目标个数有限制

    由于YOLOv1最后的输出是固定的7x7,也就是说它最多只能预测49个目标。在我前面APP的截图中可以看到标注了max为100,这说明该版本的YOLO固定输出10x10。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>在许多新的算法里,涉及到bounding box时,往往会转化成回归问题。而做出这种转变的开山之作,据我所知应该是YOLO。关于YOLO,网上有 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在许多新的算法里,涉及到bounding box时,往往会转化成回归问题。而做出这种转变的开山之作,据我所知应该是YOLO。关于YOLO,网上有 @@ -579,13 +579,13 @@ 2020-01-30T05:04:37.000Z 2020-02-01T13:24:26.263Z -

本文紧接前一篇文章machine-learning笔记:准确率和召回率,具体到目标检测中来谈一谈两个指标:AP和mAP。AP即average precision平均准确率,而mAP即mean average precision,翻译成平均平均准确率…不太好听,我且称它为均匀平均准确率吧。


目标检测中的P和R

在目标检测中,TP、FP、FN、TN有更加具体的定义,比如我们可以这样定义:
TP:IoU大于或等于阈值的检测框。
FP:IoU小于阈值的检测框。
FN:未被检测到的(没有被标注的)ground truth。
TN:由于在实验中一般不会去标注负类,因此不关心这项,可以忽略不计。
关于IoU,也就是交集比上并集的比值,如果没理解可以看看computer-vision笔记:non-max suppression这篇文章。


AP

比如有这样一个例子,总共有7个图像,其中用绿色框表示15个ground truth,用红色框表示24个我们预测的对象,并用字母标记和给出confidence。

补充:在YOLO中,confidence是这样定义的:

这里的$Pr(Object)$指的是物体先验概率,如果框内包含物体则其值为1,否则其值为0。$IoU_{pred}^{truth}$为预测框与GTbox的交并比,取值在0到1之间。因此confidence的取值范围也在0到1之间。
由该公式可知confidence反应了两个信息:

  1. 预测框内包含物体的置信度。
  2. 预测框预测的准确度。

然后我们用上面的方法判断每一个标注出来的预测对象预测得是正确的(TP)还是不正确的(FP),并根据confidence从大到小进行排列。注意,这里的Acc TP和Acc FP指的是累计的正确个数和错误个数。准确率是利用累计的正确个数比上已经统计的个数,召回率是利用累计的正确个数去比上总的正确个数(也就是15)。至于为什么这么计算,可以看看我前一篇文章的理解。

在统计完所有预测红框的准确率和召回率之后,接下来我们计算AP。利用前面计算得出的数据,我们可以画出如下统计图。

上面这种方法称为11点插值法,就是从召回率为0开始,以0.1为间隔计算准确率,因此共有11项。
如上图右下角的公式所示,我们定义这11个点中每一个点的准确率等于大于该召回率的所有点中准确率的最大值。最后累加求平均即为该分类的AP值。这种方法的缺点是它的估算有点粗糙,因此后来作了如下改进。在2012年之后(还好没有世界末日),又出现了用面积来逼近AP值的方法。

如上图中的公式所示,这里做得改变就是把插值点的精确度替换成了当前点的精确度,也就是把离散变成连续,从而可以计算面积,使得最后的平均更加有意义。


mAP

以上就是AP的两种计算方法,那么怎么来计算mAP呢?
其实AP针对的是某一类比如说人,而数据集中往往还有其他的许多类别比如说汽车、汽车人。那么分别计算每一个类的AP再求平均就得到了mAP。
注意,mAP针对的是多分类任务。mAP越高,代表精度越高。

]]>
+

本文紧接前一篇文章machine-learning笔记:准确率和召回率,具体到目标检测中来谈一谈两个指标:AP和mAP。AP即average precision平均准确率,而mAP即mean average precision,翻译成平均平均准确率…不太好听,我且称它为均匀平均准确率吧。


目标检测中的P和R

在目标检测中,TP、FP、FN、TN有更加具体的定义,比如我们可以这样定义:
TP:IoU大于或等于阈值的检测框。
FP:IoU小于阈值的检测框。
FN:未被检测到的(没有被标注的)ground truth。
TN:由于在实验中一般不会去标注负类,因此不关心这项,可以忽略不计。
关于IoU,也就是交集比上并集的比值,如果没理解可以看看computer-vision笔记:non-max suppression这篇文章。


AP

比如有这样一个例子,总共有7个图像,其中用绿色框表示15个ground truth,用红色框表示24个我们预测的对象,并用字母标记和给出confidence。

补充:在YOLO中,confidence是这样定义的:

这里的$Pr(Object)$指的是物体先验概率,如果框内包含物体则其值为1,否则其值为0。$IoU_{pred}^{truth}$为预测框与GTbox的交并比,取值在0到1之间。因此confidence的取值范围也在0到1之间。
由该公式可知confidence反应了两个信息:

  1. 预测框内包含物体的置信度。
  2. 预测框预测的准确度。

然后我们用上面的方法判断每一个标注出来的预测对象预测得是正确的(TP)还是不正确的(FP),并根据confidence从大到小进行排列。注意,这里的Acc TP和Acc FP指的是累计的正确个数和错误个数。准确率是利用累计的正确个数比上已经统计的个数,召回率是利用累计的正确个数去比上总的正确个数(也就是15)。至于为什么这么计算,可以看看我前一篇文章的理解。

在统计完所有预测红框的准确率和召回率之后,接下来我们计算AP。利用前面计算得出的数据,我们可以画出如下统计图。

上面这种方法称为11点插值法,就是从召回率为0开始,以0.1为间隔计算准确率,因此共有11项。
如上图右下角的公式所示,我们定义这11个点中每一个点的准确率等于大于该召回率的所有点中准确率的最大值。最后累加求平均即为该分类的AP值。这种方法的缺点是它的估算有点粗糙,因此后来作了如下改进。在2012年之后(还好没有世界末日),又出现了用面积来逼近AP值的方法。

如上图中的公式所示,这里做得改变就是把插值点的精确度替换成了当前点的精确度,也就是把离散变成连续,从而可以计算面积,使得最后的平均更加有意义。


mAP

以上就是AP的两种计算方法,那么怎么来计算mAP呢?
其实AP针对的是某一类比如说人,而数据集中往往还有其他的许多类别比如说汽车、汽车人。那么分别计算每一个类的AP再求平均就得到了mAP。
注意,mAP针对的是多分类任务。mAP越高,代表精度越高。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>本文紧接前一篇文章<a href="https://gsy00517.github.io/machine-learning20200130121 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>本文紧接前一篇文章<a href="https://gsy00517.github.io/machine-learning20200130121 @@ -607,13 +607,13 @@ 2020-01-30T04:18:42.000Z 2020-01-30T06:00:14.182Z -

在二分类问题中,我们常用准确率(precision)和召回率(recall)来进行评价。实际上,许多问题都可以转变为二分类问题。对于这其中的一些概念,之前都是死记硬背,今天在看目标检测的评价指标时突然有一点理解,赶快写下来。

References

参考文献:
[1]统计学习方法(第2版)


4种情况

通常我们把关注的类称为正类,其它类称为负类。而分类器在测试数据集上的预测可能是正确的也可能是不正确的。由此就出现了四种情况。
TP:True Positive(真的正类),即把正类预测为正类的个数。
FN:False Negative(假的负类),即把正类预测为负类的个数。
FP:False Positive,即把负类预测为正类的个数。
TN:True Negative,即把负类预测为负类的个数。一般不关注这一项。


准确率和召回率

由此我们可定义准确率为$P=\frac{TP}{TP+FP}$。
定义召回率为$R=\frac{TP}{TP+FN}$。
此外,还定义了$F_{1}$为准确率和召回率的调和均值,即$\frac{2}{F_{1}}=\frac{1}{P}+\frac{1}{R}$。可变换定义式求得$F_{1}$值。易得,当准确率和召回率都高时,$F_{1}$也会高。
我们可以结合下图来直观地理解一下。

结合上图和准确率与召回率地定义,由于一般在问题中我们只对我们预测的正类做标注,因此我觉得可以这样理解准确率和召回率:
准确率:预测出的结果中,有多少是正确的。
召回率:所有属于正类的目标中,有多少被正确地预测出来了。

]]>
+

在二分类问题中,我们常用准确率(precision)和召回率(recall)来进行评价。实际上,许多问题都可以转变为二分类问题。对于这其中的一些概念,之前都是死记硬背,今天在看目标检测的评价指标时突然有一点理解,赶快写下来。

References

参考文献:
[1]统计学习方法(第2版)


4种情况

通常我们把关注的类称为正类,其它类称为负类。而分类器在测试数据集上的预测可能是正确的也可能是不正确的。由此就出现了四种情况。
TP:True Positive(真的正类),即把正类预测为正类的个数。
FN:False Negative(假的负类),即把正类预测为负类的个数。
FP:False Positive,即把负类预测为正类的个数。
TN:True Negative,即把负类预测为负类的个数。一般不关注这一项。


准确率和召回率

由此我们可定义准确率为$P=\frac{TP}{TP+FP}$。
定义召回率为$R=\frac{TP}{TP+FN}$。
此外,还定义了$F_{1}$为准确率和召回率的调和均值,即$\frac{2}{F_{1}}=\frac{1}{P}+\frac{1}{R}$。可变换定义式求得$F_{1}$值。易得,当准确率和召回率都高时,$F_{1}$也会高。
我们可以结合下图来直观地理解一下。

结合上图和准确率与召回率地定义,由于一般在问题中我们只对我们预测的正类做标注,因此我觉得可以这样理解准确率和召回率:
准确率:预测出的结果中,有多少是正确的。
召回率:所有属于正类的目标中,有多少被正确地预测出来了。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>在二分类问题中,我们常用准确率(precision)和召回率(recall)来进行评价。实际上,许多问题都可以转变为二分类问题。对于这其中的一些 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在二分类问题中,我们常用准确率(precision)和召回率(recall)来进行评价。实际上,许多问题都可以转变为二分类问题。对于这其中的一些 @@ -635,13 +635,13 @@ 2020-01-29T02:29:27.000Z 2020-03-20T04:18:40.427Z -

最近在看SiamRPN系列,结果看着看着就看到Faster RCNN上面去了。尽管这个模型已经有一段时间了,我还是想把通过这两天学习的理解写下来。
RPN全称是Region Proposal Network,这里Region Proposal翻译为“区域选取”,也就是“提取候选框”的意思,所以RPN就是用来提取候选框的网络。在Faster RCNN这个结构中,RPN专门用来提取候选框,在RCNN和Fast RCNN等物体检测架构中,用来提取候选框的方法通常是比较传统的方法,而且比较耗时。而RPN一方面耗时较少,另一方面可以很容易结合到Fast RCNN中,成为一个整体。我们可以认为Faster RCNN所做的创新与改进就是用RPN结合Fast RCNN。它们三者都是based on regional proposal,即预先提取出候选区域,再通过CNN对候选区域进行样本分类(two-stage),这会影响它的速度,达不到YOLO那样的实时性,但从另一方面也保证了它的定位精度。

References

电子文献:
http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/
https://zhuanlan.zhihu.com/p/31426458
https://blog.csdn.net/lanran2/article/details/54376126


建议

在正文开始之前,推荐可以先了解一下相关的概念,比如1x1卷积、bounding box、anchor box和NMS等。我之前写过几篇相关的文章,可以先速览一下以防概念不清。
Bbox和anchor:computer-vision笔记:anchor-box
非极大抑制:computer-vision笔记:non-max suppression
1x1卷积:deep-learning笔记:着眼于深度——VGG简介与pytorch实现
此外也推荐b站上的两个视频图解RCNN和FastRCNN图解FasterRCNN,讲得挺不错的,就是声音有延迟,也可以直接看我的文章。视频中有地方讲得比较模糊,我在查阅资料之后更正了。
附faster RCNN的pytorch实现


RCNN和Fast RCNN

它们两者是Faster RCNN的先驱和基础,在这里简单介绍一下。首先先说明,无论是RCNN,还是Fast RCNN,还是Faster RCNN,它们的目的都是一样的,就是对一个图片,找出其中的目标物体,并用bounding box框出。

RCNN

RCNN即Region CNN,可以说是利用深度学习进行目标检测的开山之作,它用CNN(AlexNet)代替了之前sliding window的方法。

上面是RCNN的基本结构(图源自上面推荐的视频)。首先我们通过Selective Search(选择性搜索)去生成大量的认为可能存在目标物体的小图块。然后将所有的这些图块通过预先训练得很完美的CNN进行特征提取,比如AlexNet、VGG等。然后我们再对这些convNet卷积网络的输出进行两个操作:(1)Bbox回归,确定Bbox框出的目标位置即用回归的方式确定Bbox的4个参数;(2)分类,即用SVM判断Bbox标注的目标是什么物体。
RCNN的主要缺点就是计算成本非常巨大,这里会用上千张小图块去通过一个同样的卷积网络,即进行约2000次左右的串行式前向传播,而在之后又要分别经过回归和支持向量机两个模型,也就是要重复地执行以上操作上千遍,这就严重影响了速度。
由于使用了AlexNet(或者VGG),需要每一个候选框统一成相同的227x227的尺寸(若使用VGG,则是224x224),这就严重影响了CNN提取特征的质量。由于RCNN中SVM需要单独训练,随着类别的增加训练SVM的个数也会增加,这也使得训练过程更加复杂。此外,Selective Search去生成这些图像块的过程也是非常expensive和slow的。

补充:这里简单介绍一下Selective Search。
Selective Search类似于一种层次聚类算法,就是根据颜色、纹理、尺寸和空间交叠相似度,从众多小尺寸、细粒度的框中逐步选择、合并出大尺寸、粗粒度的约2000个候选框,用作随后的特征提取。

考虑到RCNN提取特征的巨大花费和较低的质量,后来何恺明大神对此做出了最早的改进,提出了SPP(空间金字塔池化),使得候选区域特征的提取只需要执行一遍且能够使任意大小的RoI统一成相同尺寸,其思路类似于稍晚于它的Fast RCNN(不过从日后的表现来看Fast RCNN的RoI Pooling较好),直到后来更好的Faster RCNN被提出。

Fast RCNN

Fast RCNN主要针对RCNN的问题进行了改进,即从逐个提取特征进步到了一次性提取特征。它首先直接对原始图像用卷积网络去提取特征,然后再在这张feature map上使用Selective Search。这样就使得只需要一张图像经过一遍卷积网络而不是将上千张图像去经过上千遍网络。然而Selective Search在这里还是没有得到改进,这是后面Faster RCNN使用RPN替换Selective Search的突破点。

然后通过RoI Pooling Layer使各个图像块的大小统一,以方便后面的回归和分类操作,这点后面还会讲到。最后Fast RCNN还做了一项改进就是使用两个并行的层代替了原本的SVM和Bbox回归两个模型,减少了模型的复杂度和参数量,同时这也将原本的分类、回归多任务损失统一在同一个框架中,相当于可以在训练的时候协同调整,使模型更加平衡,表现更好。


Faster RCNN

  1. 基本结构

    如图所示,Faster RCNN可以分为4个主要部分:
    1. conv layers

      作为一种CNN网络目标检测方法,Faster RCNN首先使用一组基础的conv卷积+relu激活+pooling池化提取image的feature maps。该feature maps被共享用于后续RPN层和全连接层。
    2. Region Proposal Network

      RPN网络用于生成region proposals。该层通过softmax判断anchors属于positive或者negative,再利用bounding box regression修正anchors获得精确的proposals。
    3. RoI pooling

      RoI即Region of Interest,RoI pooling是池化层的一种。该层收集输入的feature maps和proposals,综合这些信息后,提取proposal feature maps,送入后续全连接层判定目标类别。
    4. classifier

      利用proposal feature maps计算proposal的类别,即确定是什么物体。同时再次进行bounding box regression以获得检测框最终的精确位置。
  2. 流程

    1. 预处理

      首先对输入的图像进行预处理(常规操作),即减去均值并缩放成固定大小MxN。这个预处理过程对training和inference都是identical的。注意,这里的mean指的是对于整个训练集/测试集的均值而不是单张图片本身。
    2. 特征提取

      接收了处理后固定大小的图像后,使用一个卷积网络去提取特征,这个网络包含conv,pooling,relu三种层。以使用VGG16的Faster RCNN版本的网络结构为例,如图所示。 这里的conv layers部分共有13个conv层,13个relu层和4个pooling层。这里的参数值得注意:所有的conv层都设置kernel size为3,padding为1,stride为1;而所有的pooling层都设置kernel size为2且stride为2。
      这样设置有什么目的呢?可以结合下面的示意图来看,首先对所有的卷积都做了扩边处理(padding为1,即填充一圈0),使原图变为(M+2)x(N+2)的大小,然后再做3x3卷积,输出大小仍为MxN。正是这种设置,使得conv层不改变输入和输出矩阵大小。 再来看池化层,设kernel size为2且stride为2。这样每个经过pooling层的MxN矩阵,都会变为(M/2)x(N/2)大小。
      综上所述,在通过的整个卷积网络中,conv层和relu层不改变输入输出大小,只有pooling层使输出长宽都变为输入的1/2。那么4个pooling层就使得MxN大小的矩阵经过特征提取后固定变为(M/16)x(N/16)的feature maps。
    3. 提取候选框(RPN)

      接下来就是最重要的Region Proposal Network。经典的检测方法生成检测框都非常耗时,如OpenCV adaboost使用滑动窗口+图像金字塔生成检测框;或如RCNN使用SS(Selective Search)方法生成检测框。而Faster RCNN则抛弃了传统的滑动窗口和SS方法,直接使用RPN生成检测框,这也是Faster RCNN的巨大优势,能极大提升检测框的生成速度。
      这里还是先借用一张图来明确一下Faster RCNN的流程,整个RPN网络其实相当于这里的Anchor Generation Layer和Region Proposal Layer。

      这里的Anchor Target Layer是用于识别出一系列与ground truth box的分数达到一定阈值的较好的foreground anchors前景(物体)锚框和低于一定阈值的background anchors背景锚框,以及其对应的regression coefficients来训练RPN网络。这里的RPN Loss就是识别标记的foreground/background labels中的正确率与predicted和target regression coefficients之间的定义距离的组合。最后的Classification Loss也与RPN Loss定义类似,也是组合了正确率和系数距离。

      补充:这里我想先结合上面讲一下训练的过程,也可以跳过这一块继续看RPN。
      上面所述的Anchor Target Layer的输出并不用于后面分类器的训练。用于后面分类器训练的是Proposal Target Layer的输出。也就是说RPN层和分类器是分开训练的,先用预训练好的模型(比如VGG、ResNet和作者论文中用的ZF)训练RPN,再把训练好的RPN放到Faster RCNN中走上面流程图中的另一条路径训练分类器也就是整个Fast RCNN网络。根据这种思路,实际的训练过程分为4步:
      (1)在已经预训练好的model上,第一次训练RPN网络。
      (2)第一次训练整个Fast RCNN网络。
      (3)再第二次单独训练训练RPN网络。
      (4)再次利用步骤3中训练好的RPN网络,收集proposals,并第二次训练Fast RCNN网络。

      之所以只进行了类似的“循环”两次,是因为循环更多次并没有negligible improvements。

      好的还是先回到RPN模块。

      还是参照上一节提到的使用VGG16的Faster RCNN版本的网络结构,可以看到RPN网络实际分为2条线,上面一条通过softmax分类anchors获得positive anchors(存在目标的,也就是foreground anchors)和negative anchors两类,下面一条用于计算对于anchors的bounding box regression偏移量,以获得精确的proposal。而最后的Proposal Layer则负责综合positive anchors和对应bounding box regression偏移量获取proposals,同时剔除太小和超出边界的proposals。其实整个网络到了Proposal Layer这里,就相当于完成了目标定位的功能。
      下面更细地讲一下这里具体是怎么做的。

      anchor

      作者是这样生成anchor box的:对输入的feature map上的每一个点(pixel),都设置9个不同尺度和形状的anchor box,如下图所示。

      这样通过anchor box引入了目标检测中多尺度的方法,可以看到基本上整张图片上的各种尺度和形状都被cover到了。也可以事先通过对数据集聚类分析的方式来确定初始的anchor box的尺寸。
      当然,这样做获得box很不准确,不用担心,后面还有2次bounding box regression可以修正检测框的位置。

      补充:下面是原论文中的一张图,在这里做一些简单的解释。

      1. 这里的256-d指的是之前用于提取特征的卷积网络生成的feature maps的数量,其具体维度视使用的卷积网络而定。原文中使用的是ZF model中,其最后一层conv层输出维度为256,即生成256张feature maps,也相当于得到的一个feature map中每个点都是256维的。
      2. 结合前文中使用VGG16的Faster RCNN版本的网络结构,可以看到,在卷积网络提取出feature map之后,又做了3x3卷积且输出依旧是256维的,相当于每个点又融合了周围3x3的空间信息,也许这样做会是模型更鲁棒。
      3. 图中的k表示的不是千,而是每个点对应的anchor的个数,这里默认是9,而每个anhcor要分positive和negative,所以每个点cls分类需要两个分数,一个是前景(物体)分数,一个是背景分数,即图中所示2k scores;而每个anchor box又需要4个偏移量来定位,所以这里reg回归为4k coordinates。
      4. 在生成anchor的示意图中可以看到,显然anchors太多了,因此训练时会在合适的anchors中随机选取128个postive anchors与128个negative anchors进行训练。

      分类

      为了便于分析,我还是再把上面那张图拿下来。

      在经过3x3的卷积之后,又经过18个1x1的卷积核,这里的卷积主要是为了改变维数。比如我们输入一张WxH的feature map,那么经过该卷积输出就为WxHx18大小(输出图像通道数总是等于卷积核数量)。那么为什么是18呢?
      容易发现,18等于2(positive/negative)乘上9(anchors),也就是因为feature maps每一个点都有9个anchors,同时每个anchors又有可能是positive和negative,所以利用WxHx(9x2)大小的矩阵来保存这些信息。
      这里的softmax就是用于分类获得positive anchors,也就相当于初步提取了检测目标候选区域的Bbox。
      而softmax前后的两个reshape其实就是为了变换输入的张量以便于softmax分类(有点类似于一些网络在最后的卷积层和全连接层之间将张量拍扁成一维的),后面的reshape就是恢复原状的作用。
      其实RPN在这里就是在原图尺度上,设置了密密麻麻的候选anchor box。然后判断哪些是里面有目标的positive anchor,哪些是没目标的negative anchor。

      回归

      接下来我们来看看RPN模块下面那一条路径在做什么。
      如图所示,绿色框为事先标注的飞机的ground truth box(GT),红色框为前面提取的positive anchors,虽然红色框被分类器识别为飞机,但是由于红色框定位不准,依旧没有达到正确地检测出飞机的目标。所以我们希望采用一种方法对红色框进行微调,使得positive anchors和GT更加接近。

      对于一个box,我们一般使用一个四维向量$\left ( x,y,w,h \right )$来表示,即标注中心点的坐标和box的宽度和高度。在下图中,红色框A代表原始的positive anchors,绿色框G代表目标的GT,我们的目标是寻找一种关系,使得输入原始的A经过映射得到一个跟真实窗口G更接近的回归窗口G’,即寻找一种变换$F$,s.t.$F\left ( A_{x},A_{y},A_{w},A_{h} \right )=\left ( G_{x}^{‘},G_{y}^{‘},G_{w}^{‘},G_{h}^{‘} \right )$且$\left ( G_{x}^{‘},G_{y}^{‘},G_{w}^{‘},G_{h}^{‘} \right )\approx \left ( G_{x},G_{y},G_{w},G_{h} \right )$。

      那么这个变换$F$如何选择呢?一种简单的思路就是先做平移后做缩放,即:

      因此我们需要学习的是$d_{x}\left ( A \right )$,$d_{y}\left ( A \right )$,$d_{w}\left ( A \right )$,$d_{h}\left ( A \right )$这四个变换。当输入的A与GT相差较小时,可以认为这种变换是一种线性变换,那么就可以用线性回归来进行微调。

      注:线性回归就是给定输入的特征向量$X$,学习一组参数$W$,使得经过线性回归后的值跟真实值$Y$非常接近,即$Y=WX$。

      对于该问题,输入$X$是cnn feature map,定义为$\phi$;同时还有训练传入A与GT之间的变换量,即$\left ( t_{x},t_{y},t_{w},t_{h} \right )$。输出是上面所说的四个变换。则目标函数可表示为:

      为了让预测值$d_{\ast }\left ( A \right )$与真实值$t_{\ast }$差距最小,设计L1损失函数:

      得到优化目标为:

      这里的$argmin$表示的是给定参数的表达式达到最小值。

      补充:这里positive anchor与ground truth之间的平移量$\left ( t_{x},t_{y} \right )$和尺度因子$\left ( t_{w},t_{h} \right )$定义如下:

      请结合下图理解。之所以这样定义,是为了回归系数在图片进行仿射变换之后依旧能够保持不变。

      有关仿射变换,可以看一下之前的文章linear-algebra笔记:二维仿射变换

      Proposal Layer

      Proposal Layer有3个输入:positive anchors分类器结果,Bbox reg的变换量以及生成的anchor。如图所示,在选择最优的多个box,然后对这些box作NMS,结果作为proposals输出。

      RPN小结

      以上就是RPN网络提取候选框的大致介绍,总结起来就是:首先,生成anchors;然后,用softmax分类器提取positvie anchors;接着,Bbox reg回归positive anchors;最后,通过Proposal Layer生成proposals。

    4. RoI pooling

      先来看一个问题:对于传统的CNN(如AlexNet和VGG),当网络训练好后输入的图像尺寸必须是固定值,同时网络输出也是固定大小的vector或者matrix。如果输入图像大小不定,这个问题就变得比较麻烦。有2种解决办法:从图像中crop一部分传入网络,或者将图像warp成需要的大小后传入网络。 问题是,无论采取那种办法都不是很好,要么crop后破坏了图像的完整结构,要么warp后破坏了图像原始形状信息。
      于是,Fast RCNN就提出了RoI pooling来解决这个问题,其思路是与其wrap图像破坏信息,不如尝试着去wrap特征。
      其步骤如下:

      Step1:Coordinate on image

      由于proposal对应的尺度是MXN,所以首先将其映射回(M/16)X(N/16)尺度大小的feature map。(若不能整除,则向下取整,相当于丢弃小部分右侧和下侧的信息)

      Step2:Coordinate on feature map

      再将每个proposal对应的feature map区域水平分为WxH的网格。

      Step3:Coordinate on RoI feature

      接着对网格中的每一份都进行max pooling处理。如此就得到了WxH固定大小的输出。 由于RoI pooling这里采用了两次浮点数取整运算,这就使得池化之后的输出可能会于原图像的尺寸对不上,因此后来何恺明大神又提出了基于双线性插值的RoI Align来代替取整,作出了进一步改进。

      补充:2019年IoUNet又提出了PrRoI pooling,相比RoI Align,它不用预设N的个数,直接使用积分取均值。

    5. 分类器

      最后的分类部分利用已经获得的proposal feature maps,通过full connect层与softmax计算每个proposal具体属于那个类别(如人,车,电视等),输出含有各个类别的概率向量;同时再次利用bounding box regression获得每个proposal的位置偏移量,用于回归更加精确的目标检测框。
]]>
+

最近在看SiamRPN系列,结果看着看着就看到Faster RCNN上面去了。尽管这个模型已经有一段时间了,我还是想把通过这两天学习的理解写下来。
RPN全称是Region Proposal Network,这里Region Proposal翻译为“区域选取”,也就是“提取候选框”的意思,所以RPN就是用来提取候选框的网络。在Faster RCNN这个结构中,RPN专门用来提取候选框,在RCNN和Fast RCNN等物体检测架构中,用来提取候选框的方法通常是比较传统的方法,而且比较耗时。而RPN一方面耗时较少,另一方面可以很容易结合到Fast RCNN中,成为一个整体。我们可以认为Faster RCNN所做的创新与改进就是用RPN结合Fast RCNN。它们三者都是based on regional proposal,即预先提取出候选区域,再通过CNN对候选区域进行样本分类(two-stage),这会影响它的速度,达不到YOLO那样的实时性,但从另一方面也保证了它的定位精度。

References

电子文献:
http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/
https://zhuanlan.zhihu.com/p/31426458
https://blog.csdn.net/lanran2/article/details/54376126


建议

在正文开始之前,推荐可以先了解一下相关的概念,比如1x1卷积、bounding box、anchor box和NMS等。我之前写过几篇相关的文章,可以先速览一下以防概念不清。
Bbox和anchor:computer-vision笔记:anchor-box
非极大抑制:computer-vision笔记:non-max suppression
1x1卷积:deep-learning笔记:着眼于深度——VGG简介与pytorch实现
此外也推荐b站上的两个视频图解RCNN和FastRCNN图解FasterRCNN,讲得挺不错的,就是声音有延迟,也可以直接看我的文章。视频中有地方讲得比较模糊,我在查阅资料之后更正了。
附faster RCNN的pytorch实现


RCNN和Fast RCNN

它们两者是Faster RCNN的先驱和基础,在这里简单介绍一下。首先先说明,无论是RCNN,还是Fast RCNN,还是Faster RCNN,它们的目的都是一样的,就是对一个图片,找出其中的目标物体,并用bounding box框出。

RCNN

RCNN即Region CNN,可以说是利用深度学习进行目标检测的开山之作,它用CNN(AlexNet)代替了之前sliding window的方法。

上面是RCNN的基本结构(图源自上面推荐的视频)。首先我们通过Selective Search(选择性搜索)去生成大量的认为可能存在目标物体的小图块。然后将所有的这些图块通过预先训练得很完美的CNN进行特征提取,比如AlexNet、VGG等。然后我们再对这些convNet卷积网络的输出进行两个操作:(1)Bbox回归,确定Bbox框出的目标位置即用回归的方式确定Bbox的4个参数;(2)分类,即用SVM判断Bbox标注的目标是什么物体。
RCNN的主要缺点就是计算成本非常巨大,这里会用上千张小图块去通过一个同样的卷积网络,即进行约2000次左右的串行式前向传播,而在之后又要分别经过回归和支持向量机两个模型,也就是要重复地执行以上操作上千遍,这就严重影响了速度。
由于使用了AlexNet(或者VGG),需要每一个候选框统一成相同的227x227的尺寸(若使用VGG,则是224x224),这就严重影响了CNN提取特征的质量。由于RCNN中SVM需要单独训练,随着类别的增加训练SVM的个数也会增加,这也使得训练过程更加复杂。此外,Selective Search去生成这些图像块的过程也是非常expensive和slow的。

补充:这里简单介绍一下Selective Search。
Selective Search类似于一种层次聚类算法,就是根据颜色、纹理、尺寸和空间交叠相似度,从众多小尺寸、细粒度的框中逐步选择、合并出大尺寸、粗粒度的约2000个候选框,用作随后的特征提取。

考虑到RCNN提取特征的巨大花费和较低的质量,后来何恺明大神对此做出了最早的改进,提出了SPP(空间金字塔池化),使得候选区域特征的提取只需要执行一遍且能够使任意大小的RoI统一成相同尺寸,其思路类似于稍晚于它的Fast RCNN(不过从日后的表现来看Fast RCNN的RoI Pooling较好),直到后来更好的Faster RCNN被提出。

Fast RCNN

Fast RCNN主要针对RCNN的问题进行了改进,即从逐个提取特征进步到了一次性提取特征。它首先直接对原始图像用卷积网络去提取特征,然后再在这张feature map上使用Selective Search。这样就使得只需要一张图像经过一遍卷积网络而不是将上千张图像去经过上千遍网络。然而Selective Search在这里还是没有得到改进,这是后面Faster RCNN使用RPN替换Selective Search的突破点。

然后通过RoI Pooling Layer使各个图像块的大小统一,以方便后面的回归和分类操作,这点后面还会讲到。最后Fast RCNN还做了一项改进就是使用两个并行的层代替了原本的SVM和Bbox回归两个模型,减少了模型的复杂度和参数量,同时这也将原本的分类、回归多任务损失统一在同一个框架中,相当于可以在训练的时候协同调整,使模型更加平衡,表现更好。


Faster RCNN

  1. 基本结构

    如图所示,Faster RCNN可以分为4个主要部分:
    1. conv layers

      作为一种CNN网络目标检测方法,Faster RCNN首先使用一组基础的conv卷积+relu激活+pooling池化提取image的feature maps。该feature maps被共享用于后续RPN层和全连接层。
    2. Region Proposal Network

      RPN网络用于生成region proposals。该层通过softmax判断anchors属于positive或者negative,再利用bounding box regression修正anchors获得精确的proposals。
    3. RoI pooling

      RoI即Region of Interest,RoI pooling是池化层的一种。该层收集输入的feature maps和proposals,综合这些信息后,提取proposal feature maps,送入后续全连接层判定目标类别。
    4. classifier

      利用proposal feature maps计算proposal的类别,即确定是什么物体。同时再次进行bounding box regression以获得检测框最终的精确位置。
  2. 流程

    1. 预处理

      首先对输入的图像进行预处理(常规操作),即减去均值并缩放成固定大小MxN。这个预处理过程对training和inference都是identical的。注意,这里的mean指的是对于整个训练集/测试集的均值而不是单张图片本身。
    2. 特征提取

      接收了处理后固定大小的图像后,使用一个卷积网络去提取特征,这个网络包含conv,pooling,relu三种层。以使用VGG16的Faster RCNN版本的网络结构为例,如图所示。 这里的conv layers部分共有13个conv层,13个relu层和4个pooling层。这里的参数值得注意:所有的conv层都设置kernel size为3,padding为1,stride为1;而所有的pooling层都设置kernel size为2且stride为2。
      这样设置有什么目的呢?可以结合下面的示意图来看,首先对所有的卷积都做了扩边处理(padding为1,即填充一圈0),使原图变为(M+2)x(N+2)的大小,然后再做3x3卷积,输出大小仍为MxN。正是这种设置,使得conv层不改变输入和输出矩阵大小。 再来看池化层,设kernel size为2且stride为2。这样每个经过pooling层的MxN矩阵,都会变为(M/2)x(N/2)大小。
      综上所述,在通过的整个卷积网络中,conv层和relu层不改变输入输出大小,只有pooling层使输出长宽都变为输入的1/2。那么4个pooling层就使得MxN大小的矩阵经过特征提取后固定变为(M/16)x(N/16)的feature maps。
    3. 提取候选框(RPN)

      接下来就是最重要的Region Proposal Network。经典的检测方法生成检测框都非常耗时,如OpenCV adaboost使用滑动窗口+图像金字塔生成检测框;或如RCNN使用SS(Selective Search)方法生成检测框。而Faster RCNN则抛弃了传统的滑动窗口和SS方法,直接使用RPN生成检测框,这也是Faster RCNN的巨大优势,能极大提升检测框的生成速度。
      这里还是先借用一张图来明确一下Faster RCNN的流程,整个RPN网络其实相当于这里的Anchor Generation Layer和Region Proposal Layer。

      这里的Anchor Target Layer是用于识别出一系列与ground truth box的分数达到一定阈值的较好的foreground anchors前景(物体)锚框和低于一定阈值的background anchors背景锚框,以及其对应的regression coefficients来训练RPN网络。这里的RPN Loss就是识别标记的foreground/background labels中的正确率与predicted和target regression coefficients之间的定义距离的组合。最后的Classification Loss也与RPN Loss定义类似,也是组合了正确率和系数距离。

      补充:这里我想先结合上面讲一下训练的过程,也可以跳过这一块继续看RPN。
      上面所述的Anchor Target Layer的输出并不用于后面分类器的训练。用于后面分类器训练的是Proposal Target Layer的输出。也就是说RPN层和分类器是分开训练的,先用预训练好的模型(比如VGG、ResNet和作者论文中用的ZF)训练RPN,再把训练好的RPN放到Faster RCNN中走上面流程图中的另一条路径训练分类器也就是整个Fast RCNN网络。根据这种思路,实际的训练过程分为4步:
      (1)在已经预训练好的model上,第一次训练RPN网络。
      (2)第一次训练整个Fast RCNN网络。
      (3)再第二次单独训练训练RPN网络。
      (4)再次利用步骤3中训练好的RPN网络,收集proposals,并第二次训练Fast RCNN网络。

      之所以只进行了类似的“循环”两次,是因为循环更多次并没有negligible improvements。

      好的还是先回到RPN模块。

      还是参照上一节提到的使用VGG16的Faster RCNN版本的网络结构,可以看到RPN网络实际分为2条线,上面一条通过softmax分类anchors获得positive anchors(存在目标的,也就是foreground anchors)和negative anchors两类,下面一条用于计算对于anchors的bounding box regression偏移量,以获得精确的proposal。而最后的Proposal Layer则负责综合positive anchors和对应bounding box regression偏移量获取proposals,同时剔除太小和超出边界的proposals。其实整个网络到了Proposal Layer这里,就相当于完成了目标定位的功能。
      下面更细地讲一下这里具体是怎么做的。

      anchor

      作者是这样生成anchor box的:对输入的feature map上的每一个点(pixel),都设置9个不同尺度和形状的anchor box,如下图所示。

      这样通过anchor box引入了目标检测中多尺度的方法,可以看到基本上整张图片上的各种尺度和形状都被cover到了。也可以事先通过对数据集聚类分析的方式来确定初始的anchor box的尺寸。
      当然,这样做获得box很不准确,不用担心,后面还有2次bounding box regression可以修正检测框的位置。

      补充:下面是原论文中的一张图,在这里做一些简单的解释。

      1. 这里的256-d指的是之前用于提取特征的卷积网络生成的feature maps的数量,其具体维度视使用的卷积网络而定。原文中使用的是ZF model中,其最后一层conv层输出维度为256,即生成256张feature maps,也相当于得到的一个feature map中每个点都是256维的。
      2. 结合前文中使用VGG16的Faster RCNN版本的网络结构,可以看到,在卷积网络提取出feature map之后,又做了3x3卷积且输出依旧是256维的,相当于每个点又融合了周围3x3的空间信息,也许这样做会是模型更鲁棒。
      3. 图中的k表示的不是千,而是每个点对应的anchor的个数,这里默认是9,而每个anhcor要分positive和negative,所以每个点cls分类需要两个分数,一个是前景(物体)分数,一个是背景分数,即图中所示2k scores;而每个anchor box又需要4个偏移量来定位,所以这里reg回归为4k coordinates。
      4. 在生成anchor的示意图中可以看到,显然anchors太多了,因此训练时会在合适的anchors中随机选取128个postive anchors与128个negative anchors进行训练。

      分类

      为了便于分析,我还是再把上面那张图拿下来。

      在经过3x3的卷积之后,又经过18个1x1的卷积核,这里的卷积主要是为了改变维数。比如我们输入一张WxH的feature map,那么经过该卷积输出就为WxHx18大小(输出图像通道数总是等于卷积核数量)。那么为什么是18呢?
      容易发现,18等于2(positive/negative)乘上9(anchors),也就是因为feature maps每一个点都有9个anchors,同时每个anchors又有可能是positive和negative,所以利用WxHx(9x2)大小的矩阵来保存这些信息。
      这里的softmax就是用于分类获得positive anchors,也就相当于初步提取了检测目标候选区域的Bbox。
      而softmax前后的两个reshape其实就是为了变换输入的张量以便于softmax分类(有点类似于一些网络在最后的卷积层和全连接层之间将张量拍扁成一维的),后面的reshape就是恢复原状的作用。
      其实RPN在这里就是在原图尺度上,设置了密密麻麻的候选anchor box。然后判断哪些是里面有目标的positive anchor,哪些是没目标的negative anchor。

      回归

      接下来我们来看看RPN模块下面那一条路径在做什么。
      如图所示,绿色框为事先标注的飞机的ground truth box(GT),红色框为前面提取的positive anchors,虽然红色框被分类器识别为飞机,但是由于红色框定位不准,依旧没有达到正确地检测出飞机的目标。所以我们希望采用一种方法对红色框进行微调,使得positive anchors和GT更加接近。

      对于一个box,我们一般使用一个四维向量$\left ( x,y,w,h \right )$来表示,即标注中心点的坐标和box的宽度和高度。在下图中,红色框A代表原始的positive anchors,绿色框G代表目标的GT,我们的目标是寻找一种关系,使得输入原始的A经过映射得到一个跟真实窗口G更接近的回归窗口G’,即寻找一种变换$F$,s.t.$F\left ( A_{x},A_{y},A_{w},A_{h} \right )=\left ( G_{x}^{‘},G_{y}^{‘},G_{w}^{‘},G_{h}^{‘} \right )$且$\left ( G_{x}^{‘},G_{y}^{‘},G_{w}^{‘},G_{h}^{‘} \right )\approx \left ( G_{x},G_{y},G_{w},G_{h} \right )$。

      那么这个变换$F$如何选择呢?一种简单的思路就是先做平移后做缩放,即:

      因此我们需要学习的是$d_{x}\left ( A \right )$,$d_{y}\left ( A \right )$,$d_{w}\left ( A \right )$,$d_{h}\left ( A \right )$这四个变换。当输入的A与GT相差较小时,可以认为这种变换是一种线性变换,那么就可以用线性回归来进行微调。

      注:线性回归就是给定输入的特征向量$X$,学习一组参数$W$,使得经过线性回归后的值跟真实值$Y$非常接近,即$Y=WX$。

      对于该问题,输入$X$是cnn feature map,定义为$\phi$;同时还有训练传入A与GT之间的变换量,即$\left ( t_{x},t_{y},t_{w},t_{h} \right )$。输出是上面所说的四个变换。则目标函数可表示为:

      为了让预测值$d_{\ast }\left ( A \right )$与真实值$t_{\ast }$差距最小,设计L1损失函数:

      得到优化目标为:

      这里的$argmin$表示的是给定参数的表达式达到最小值。

      补充:这里positive anchor与ground truth之间的平移量$\left ( t_{x},t_{y} \right )$和尺度因子$\left ( t_{w},t_{h} \right )$定义如下:

      请结合下图理解。之所以这样定义,是为了回归系数在图片进行仿射变换之后依旧能够保持不变。

      有关仿射变换,可以看一下之前的文章linear-algebra笔记:二维仿射变换

      Proposal Layer

      Proposal Layer有3个输入:positive anchors分类器结果,Bbox reg的变换量以及生成的anchor。如图所示,在选择最优的多个box,然后对这些box作NMS,结果作为proposals输出。

      RPN小结

      以上就是RPN网络提取候选框的大致介绍,总结起来就是:首先,生成anchors;然后,用softmax分类器提取positvie anchors;接着,Bbox reg回归positive anchors;最后,通过Proposal Layer生成proposals。

    4. RoI pooling

      先来看一个问题:对于传统的CNN(如AlexNet和VGG),当网络训练好后输入的图像尺寸必须是固定值,同时网络输出也是固定大小的vector或者matrix。如果输入图像大小不定,这个问题就变得比较麻烦。有2种解决办法:从图像中crop一部分传入网络,或者将图像warp成需要的大小后传入网络。 问题是,无论采取那种办法都不是很好,要么crop后破坏了图像的完整结构,要么warp后破坏了图像原始形状信息。
      于是,Fast RCNN就提出了RoI pooling来解决这个问题,其思路是与其wrap图像破坏信息,不如尝试着去wrap特征。
      其步骤如下:

      Step1:Coordinate on image

      由于proposal对应的尺度是MXN,所以首先将其映射回(M/16)X(N/16)尺度大小的feature map。(若不能整除,则向下取整,相当于丢弃小部分右侧和下侧的信息)

      Step2:Coordinate on feature map

      再将每个proposal对应的feature map区域水平分为WxH的网格。

      Step3:Coordinate on RoI feature

      接着对网格中的每一份都进行max pooling处理。如此就得到了WxH固定大小的输出。 由于RoI pooling这里采用了两次浮点数取整运算,这就使得池化之后的输出可能会于原图像的尺寸对不上,因此后来何恺明大神又提出了基于双线性插值的RoI Align来代替取整,作出了进一步改进。

      补充:2019年IoUNet又提出了PrRoI pooling,相比RoI Align,它不用预设N的个数,直接使用积分取均值。

    5. 分类器

      最后的分类部分利用已经获得的proposal feature maps,通过full connect层与softmax计算每个proposal具体属于那个类别(如人,车,电视等),输出含有各个类别的概率向量;同时再次利用bounding box regression获得每个proposal的位置偏移量,用于回归更加精确的目标检测框。
]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>最近在看SiamRPN系列,结果看着看着就看到Faster RCNN上面去了。尽管这个模型已经有一段时间了,我还是想把通过这两天学习的理解写下来 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>最近在看SiamRPN系列,结果看着看着就看到Faster RCNN上面去了。尽管这个模型已经有一段时间了,我还是想把通过这两天学习的理解写下来 @@ -665,13 +665,13 @@ 2020-01-28T08:24:22.000Z 2020-02-13T15:25:40.596Z -

Non-max Suppression即非极大值抑制,可简写为NMS,顾名思义就是抑制不是极大值,可以理解为局部最大搜索。由于在目标检测时,我们的算法可能会对同一个对象做出多次检测。我们的目标就是要去除冗余的检测框,仅保留最好的一个。这时就可以采用非极大值抑制的方法来确保算法对每个对象只检测一次。
如果本文阅读时有不懂之处,可以先看一下computer-vision笔记:anchor-box

References

电子文献:
https://www.cnblogs.com/makefile/p/nms.html


交并比(IoU)

或许在之前你已经看到过这个名词,比如computer-vision笔记:anchor-box。这里就简单介绍一下交并比。
可直接根据字面意思理解,下面直接通过一张图介绍,看完就懂。

实际上,IoU也是存在一定缺陷的,详见computer-vision笔记:IoU与GIoU


非极大值抑制(NMS)

非极大值抑制主要可以分为如下几步:

  1. 抛弃概率很低的预测

    这一步在网上大多数的文章中都没有被提及,但我认为是有必要的。因为有可能会存在着孤立的bounding box,它不会被抑制掉但它的概率很低,而它的内部的确没有框出目标,这是我们不希望的情况。因此非极大值抑制的第一步就是抛弃概率很低的预测,因为它们很有可能不包含任何目标。比如,我们可以抛弃$p_{c}< 0.5$的所有box。
  2. 选取概率最大的box并对其它box进行抑制

    在剩余的一系列box(记为$B$)中,选取概率$p_{c}$(这里是0到1之间的一个数)最大的box,并把它作为最终要输出的一个预测,从$B$中移除。
    同时我们从$B$中移除和刚刚选出的box的IoU达到一定阈值的box,因为它们很有可能在标注同一个目标。比如,我们可以把IoU大于阈值0.4的box都舍去。
  3. 重复直到列表为空

    重复第二步操作,也就是寻找剩余$B$中的下一个概率最大的box,并把它作为输出从$B$中移除,同时用它对它周围的box进行抑制。以此类推,直到$B$中不含未处理的box,即所有的有效预测均已输出。在图片所示的例子中,我们共需要进行两次循环,最终输出两个预测:人和汽车。

可用如下伪代码表示NMS的处理过程。

]]>
+

Non-max Suppression即非极大值抑制,可简写为NMS,顾名思义就是抑制不是极大值,可以理解为局部最大搜索。由于在目标检测时,我们的算法可能会对同一个对象做出多次检测。我们的目标就是要去除冗余的检测框,仅保留最好的一个。这时就可以采用非极大值抑制的方法来确保算法对每个对象只检测一次。
如果本文阅读时有不懂之处,可以先看一下computer-vision笔记:anchor-box

References

电子文献:
https://www.cnblogs.com/makefile/p/nms.html


交并比(IoU)

或许在之前你已经看到过这个名词,比如computer-vision笔记:anchor-box。这里就简单介绍一下交并比。
可直接根据字面意思理解,下面直接通过一张图介绍,看完就懂。

实际上,IoU也是存在一定缺陷的,详见computer-vision笔记:IoU与GIoU


非极大值抑制(NMS)

非极大值抑制主要可以分为如下几步:

  1. 抛弃概率很低的预测

    这一步在网上大多数的文章中都没有被提及,但我认为是有必要的。因为有可能会存在着孤立的bounding box,它不会被抑制掉但它的概率很低,而它的内部的确没有框出目标,这是我们不希望的情况。因此非极大值抑制的第一步就是抛弃概率很低的预测,因为它们很有可能不包含任何目标。比如,我们可以抛弃$p_{c}< 0.5$的所有box。
  2. 选取概率最大的box并对其它box进行抑制

    在剩余的一系列box(记为$B$)中,选取概率$p_{c}$(这里是0到1之间的一个数)最大的box,并把它作为最终要输出的一个预测,从$B$中移除。
    同时我们从$B$中移除和刚刚选出的box的IoU达到一定阈值的box,因为它们很有可能在标注同一个目标。比如,我们可以把IoU大于阈值0.4的box都舍去。
  3. 重复直到列表为空

    重复第二步操作,也就是寻找剩余$B$中的下一个概率最大的box,并把它作为输出从$B$中移除,同时用它对它周围的box进行抑制。以此类推,直到$B$中不含未处理的box,即所有的有效预测均已输出。在图片所示的例子中,我们共需要进行两次循环,最终输出两个预测:人和汽车。

可用如下伪代码表示NMS的处理过程。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>Non-max Suppression即非极大值抑制,可简写为NMS,顾名思义就是抑制不是极大值,可以理解为局部最大搜索。由于在目标检测时,我们 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>Non-max Suppression即非极大值抑制,可简写为NMS,顾名思义就是抑制不是极大值,可以理解为局部最大搜索。由于在目标检测时,我们 @@ -691,13 +691,13 @@ 2020-01-28T08:23:33.000Z 2020-02-01T13:22:48.872Z -

之前在deep-learning笔记:着眼于深度——VGG简介与pytorch实现一文中提到了anchor box这一个概念,在我阅读YOLO的论文时,也遇到了这个名词。搞懂之后觉得还是写一下比较好。


bounding box的向量表示

在目标检测和目标跟踪等领域中,对于目标物体的位置和大小我们往往会使用bounding box来框出表示(有时为方便也简写为Bbox,注意不是那个打节拍的口技虽然我也在学哈哈)。下面以目标检测为例,介绍一下如何用向量表示bounding box。
我们一般取图片的左上角为(0,0),取图片的右下角为(1,1)。假如现在我们要对下面这个图像中的目标进行检测,检测该图中是否存在三大将即黄猿、赤犬、青雉。

我们可以使用这样一个8维的向量来表示图片中红色框即输出的bounding box。

其中$p_{c}$表示的是识别目标存在的概率,这里可以简化成1(存在目标)和0(不存在目标)。倘若$p_{c}=0$,也就是认为识别区域中不存在目标,那么向量中后面的7个参数就都是无意义项(don’t cares)。

注意:因为没有进行分割,这里的识别区域是整个图像,而在实际应用中往往进行了较为精细地分割以提高检测效果。

$b_{x}$和$b_{y}$指的是目标所在中心点的坐标,也就是bounding box的中点坐标。注意,这两个坐标的取值必须为0到1之间的数,且坐标系取图片的左上角为(0,0),取图片的右下角为(1,1),千万别搞错了。
$b_{h}$和$b_{w}$指的是bouding box占识别区域总长、宽的比值,在这个例子中取值在0到1之间,但要注意的是,它们的取值也可以超过1,也就是物体的大小超出了识别区域(这在分割图像后可能发生,可以看一下后文图例)。这就相当于以每个识别区域为单位1,我觉得这也是为什么之前设定坐标系时取右下角为(1,1)而不是其他数值的原因之一吧。
这个用于表示bounding box的向量的长度可以这样来计算:length = 5 + 待检测目标类别的总数。这时因为这里的$c_{1}c_{2}c_{3}$采用的是one-hot编码,当$p_{c}=1$时,这三个参数有且仅有一个值为1,即一个bounding box只能表示一个目标。比如bounding box中圈出的是赤犬,那么我们就可以将$c_{1}c_{2}c_{3}$表示为010。
以上文中的图片为例,其bounding box的向量表示应该如下所示。


anchor box

上文提到,我们可以将图像进行分割,以提高检测的效果。注意,这些分割是隐式的。在YOLO等algorithm中,一般有这样的规则,即物体的中心点在哪一个格子内,哪一个格子就负责检测这个物体。这就会导致一个问题。由于我们的检测方式是每一个识别区域(即分割的格子)都只输出一个向量表示其中的bounding box或者不存在目标($p_{c}=0$),因此当检测格子分割得较少时,可能会存在两个目标物体的中点同属一个格子而只能表示出一个物体的问题。这就需要引入anchor box。

比如在上图中,我们对图像进行3x3的分割,假如我们要识别汽车、人、汽车人这三类物体(也就是说要用到8维的向量),而汽车和人的中点都位于同一个格子中。这时再用之前的方法是无法同时识别人和汽车的。这时,我们可以预先定义两个不同形状的anchor box。

注意:在实际应用中往往会指定更多个anchor box。

此时,每个格子的输出向量就要表示成如下形式。此时向量的长度就要这样来计算:length = (5 + 待检测目标类别的总数) x anchor数

为了好看,我们用转置表示。

这里前8个参数是和竖着的anchor box1相关联的,后8个参数是和横着的anchor box2相关联的。
这样,anchor box1与人更近似,我们就用前8个参数标注人的bounding box,anchor box2与汽车更接近,我们就用后8个参数标注汽车的bounding box。此时这里的$b_{h}$和$b_{w}$指的就是anchor box之于格子的比值。因此,我们可以通过增加不同长宽比和尺寸的anchor box使得检测更加具有针对性。
倘若这张图片种只有汽车,那么我们选择与汽车的IoU(交并比)最大的anchor box。比如这里我们就是将第一个$p_{c}$设为0,随后的7个参数都成了无意义项,第二个$p_{c}$设为1,用最后的7个参数标注汽车也就是anchor box2。
一般情况下,我们可以通过更精细地分割图片来大大降低同一格子中同时出现两个物体的中点的可能。不过anchor box还有更多不错的效果值得借鉴,比如可以在目标跟踪中应对目标尺度大小的变化。


选取

关于如何选取anchor box,主要有三种方法:

  1. 人为经验选取

    当我们知道需要预测的目标类型时,我们往往可以合理地使用人工指定的方法设置一系列anchor box。比如设置扁而宽的用于检测汽车,设置长而窄的用于检测人,设置高而大的用于检测汽车人。
  2. 聚类

    这种方法在后期YOLO论文中有很好的使用,即利用机器学习中的k-means算法,将两类对象形状进行聚类,选择最具有代表性的一组anchor box来代表我们试图检测的一组对象类别。
  3. 作为参数学习

    即作为参数学习。
]]>
+

之前在deep-learning笔记:着眼于深度——VGG简介与pytorch实现一文中提到了anchor box这一个概念,在我阅读YOLO的论文时,也遇到了这个名词。搞懂之后觉得还是写一下比较好。


bounding box的向量表示

在目标检测和目标跟踪等领域中,对于目标物体的位置和大小我们往往会使用bounding box来框出表示(有时为方便也简写为Bbox,注意不是那个打节拍的口技虽然我也在学哈哈)。下面以目标检测为例,介绍一下如何用向量表示bounding box。
我们一般取图片的左上角为(0,0),取图片的右下角为(1,1)。假如现在我们要对下面这个图像中的目标进行检测,检测该图中是否存在三大将即黄猿、赤犬、青雉。

我们可以使用这样一个8维的向量来表示图片中红色框即输出的bounding box。

其中$p_{c}$表示的是识别目标存在的概率,这里可以简化成1(存在目标)和0(不存在目标)。倘若$p_{c}=0$,也就是认为识别区域中不存在目标,那么向量中后面的7个参数就都是无意义项(don’t cares)。

注意:因为没有进行分割,这里的识别区域是整个图像,而在实际应用中往往进行了较为精细地分割以提高检测效果。

$b_{x}$和$b_{y}$指的是目标所在中心点的坐标,也就是bounding box的中点坐标。注意,这两个坐标的取值必须为0到1之间的数,且坐标系取图片的左上角为(0,0),取图片的右下角为(1,1),千万别搞错了。
$b_{h}$和$b_{w}$指的是bouding box占识别区域总长、宽的比值,在这个例子中取值在0到1之间,但要注意的是,它们的取值也可以超过1,也就是物体的大小超出了识别区域(这在分割图像后可能发生,可以看一下后文图例)。这就相当于以每个识别区域为单位1,我觉得这也是为什么之前设定坐标系时取右下角为(1,1)而不是其他数值的原因之一吧。
这个用于表示bounding box的向量的长度可以这样来计算:length = 5 + 待检测目标类别的总数。这时因为这里的$c_{1}c_{2}c_{3}$采用的是one-hot编码,当$p_{c}=1$时,这三个参数有且仅有一个值为1,即一个bounding box只能表示一个目标。比如bounding box中圈出的是赤犬,那么我们就可以将$c_{1}c_{2}c_{3}$表示为010。
以上文中的图片为例,其bounding box的向量表示应该如下所示。


anchor box

上文提到,我们可以将图像进行分割,以提高检测的效果。注意,这些分割是隐式的。在YOLO等algorithm中,一般有这样的规则,即物体的中心点在哪一个格子内,哪一个格子就负责检测这个物体。这就会导致一个问题。由于我们的检测方式是每一个识别区域(即分割的格子)都只输出一个向量表示其中的bounding box或者不存在目标($p_{c}=0$),因此当检测格子分割得较少时,可能会存在两个目标物体的中点同属一个格子而只能表示出一个物体的问题。这就需要引入anchor box。

比如在上图中,我们对图像进行3x3的分割,假如我们要识别汽车、人、汽车人这三类物体(也就是说要用到8维的向量),而汽车和人的中点都位于同一个格子中。这时再用之前的方法是无法同时识别人和汽车的。这时,我们可以预先定义两个不同形状的anchor box。

注意:在实际应用中往往会指定更多个anchor box。

此时,每个格子的输出向量就要表示成如下形式。此时向量的长度就要这样来计算:length = (5 + 待检测目标类别的总数) x anchor数

为了好看,我们用转置表示。

这里前8个参数是和竖着的anchor box1相关联的,后8个参数是和横着的anchor box2相关联的。
这样,anchor box1与人更近似,我们就用前8个参数标注人的bounding box,anchor box2与汽车更接近,我们就用后8个参数标注汽车的bounding box。此时这里的$b_{h}$和$b_{w}$指的就是anchor box之于格子的比值。因此,我们可以通过增加不同长宽比和尺寸的anchor box使得检测更加具有针对性。
倘若这张图片种只有汽车,那么我们选择与汽车的IoU(交并比)最大的anchor box。比如这里我们就是将第一个$p_{c}$设为0,随后的7个参数都成了无意义项,第二个$p_{c}$设为1,用最后的7个参数标注汽车也就是anchor box2。
一般情况下,我们可以通过更精细地分割图片来大大降低同一格子中同时出现两个物体的中点的可能。不过anchor box还有更多不错的效果值得借鉴,比如可以在目标跟踪中应对目标尺度大小的变化。


选取

关于如何选取anchor box,主要有三种方法:

  1. 人为经验选取

    当我们知道需要预测的目标类型时,我们往往可以合理地使用人工指定的方法设置一系列anchor box。比如设置扁而宽的用于检测汽车,设置长而窄的用于检测人,设置高而大的用于检测汽车人。
  2. 聚类

    这种方法在后期YOLO论文中有很好的使用,即利用机器学习中的k-means算法,将两类对象形状进行聚类,选择最具有代表性的一组anchor box来代表我们试图检测的一组对象类别。
  3. 作为参数学习

    即作为参数学习。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>之前在<a href="https://gsy00517.github.io/deep-learning20190915073809/" tar + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前在<a href="https://gsy00517.github.io/deep-learning20190915073809/" tar @@ -719,13 +719,13 @@ 2020-01-28T06:36:52.000Z 2020-02-15T14:09:44.271Z -

迁移学习(Transfer Learning),又称预训练。即利用社区内开源的权重参数更快更好地训练自己的网络。然而,我一直纳闷的是,别人训练好的参数是怎么直接用到自己的网络上来的,倘若网络结构内部有一点不同那岂不是完全不一样了吗?Andrew Ng的课程给了我很大的启发,结合上自己的一些想法,写下来。

References

电子文献:
http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/#Network_Architecture


原理

这里摘录上面参考资料中的一段话,我觉得说得很明白了。
Using a network trained on one dataset on a different problem is possible because neural networks exhibit “transfer learning”. The first few layers of the network learn to detect general features such as edges and color blobs that are good discriminating features across many different problems. The features learnt by the later layers are higher level, more problem specific features. These layers can either be removed or the weights for these layers can be fine-tuned during back-propagation.


如何使用迁移学习

迁移学习主要有如下三种策略。

下面我们具体情况具体分析。

  1. 训练数据较少

    首先,我们把开源的代码(即网络结构)和对应的权重都下载下来。当手头的训练集较小时,我们可以冻结所有层的参数,去掉网络中的softmax层或者其它与最后输出相联系的层,并且创建自己的softmax单元。这主要是考虑到下载的模型所对应的输出类别或者其他的需求与我们的不一致。在训练过程中,我们保持之前所有层冻结(用于特征提取等),只训练和我们自己设计的softmax层有关的参数。
    其实这就相当于用预训练的网络构成一个映射关系,对每一个输入都能产生一个特征向量。然后用自己设计的一个很浅的softmax网络对这些特征向量做预测。

    注:这些特征向量可以存到硬盘中,以节约每次都要遍历训练集重新计算这个激活值的时间。这在用Siamese网络进行人脸识别时是一个较为常用的操作。

  2. 训练数据中等

    如果数据较多,我们可以冻结较少的层。根据上文所述的原理,我们一般冻结较为基础的前面的几层。对于后面的层,我们有两种方法。
    (1)可以加载权重作为初始化,然后用同样的结构和自己的数据集继续训练。
    (2)也可以直接去掉这几层,换成我们自己的隐藏单元和自己的输出层。
    其实我觉得这里的基本思想还是相当于把冻结的那几层看成一个关于特征的映射。
  3. 训练数据较多

    若有足够多的数据用来作训练集,我们这时就可以把所有的参数都仅用来初始化,然后训练整个网络。因为此时不用担心过拟合的问题。
    其实规律就是:拥有越多的数据,我们需要冻结的层数(参数)越少,我们能够训练的层数(参数)就越多。

除了训练数据量之外,新数据集与原数据集的相似度也是一个需要考虑的点。倘若我们拥有较多的数据,而新数据集与原数据集的相似度却很低时,我们最好不要使用迁移学习进行预训练,而是从头开始训练整个网络。


为什么要迁移学习

关于使用迁移学习的原因,Andrew Ng没有提及,不多简单想来主要的原因主要有如下几点:

  1. 迁移学习可以弥补训练数据的不足。
  2. 迁移学习可以大大减少训练时间。
  3. 迁移学习(特别是同一领域相关的参数)可以有效地防止梯度下降卡在局部最优解处,一定程度上保证了算法的收敛性。
]]>
+

迁移学习(Transfer Learning),又称预训练。即利用社区内开源的权重参数更快更好地训练自己的网络。然而,我一直纳闷的是,别人训练好的参数是怎么直接用到自己的网络上来的,倘若网络结构内部有一点不同那岂不是完全不一样了吗?Andrew Ng的课程给了我很大的启发,结合上自己的一些想法,写下来。

References

电子文献:
http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/#Network_Architecture


原理

这里摘录上面参考资料中的一段话,我觉得说得很明白了。
Using a network trained on one dataset on a different problem is possible because neural networks exhibit “transfer learning”. The first few layers of the network learn to detect general features such as edges and color blobs that are good discriminating features across many different problems. The features learnt by the later layers are higher level, more problem specific features. These layers can either be removed or the weights for these layers can be fine-tuned during back-propagation.


如何使用迁移学习

迁移学习主要有如下三种策略。

下面我们具体情况具体分析。

  1. 训练数据较少

    首先,我们把开源的代码(即网络结构)和对应的权重都下载下来。当手头的训练集较小时,我们可以冻结所有层的参数,去掉网络中的softmax层或者其它与最后输出相联系的层,并且创建自己的softmax单元。这主要是考虑到下载的模型所对应的输出类别或者其他的需求与我们的不一致。在训练过程中,我们保持之前所有层冻结(用于特征提取等),只训练和我们自己设计的softmax层有关的参数。
    其实这就相当于用预训练的网络构成一个映射关系,对每一个输入都能产生一个特征向量。然后用自己设计的一个很浅的softmax网络对这些特征向量做预测。

    注:这些特征向量可以存到硬盘中,以节约每次都要遍历训练集重新计算这个激活值的时间。这在用Siamese网络进行人脸识别时是一个较为常用的操作。

  2. 训练数据中等

    如果数据较多,我们可以冻结较少的层。根据上文所述的原理,我们一般冻结较为基础的前面的几层。对于后面的层,我们有两种方法。
    (1)可以加载权重作为初始化,然后用同样的结构和自己的数据集继续训练。
    (2)也可以直接去掉这几层,换成我们自己的隐藏单元和自己的输出层。
    其实我觉得这里的基本思想还是相当于把冻结的那几层看成一个关于特征的映射。
  3. 训练数据较多

    若有足够多的数据用来作训练集,我们这时就可以把所有的参数都仅用来初始化,然后训练整个网络。因为此时不用担心过拟合的问题。
    其实规律就是:拥有越多的数据,我们需要冻结的层数(参数)越少,我们能够训练的层数(参数)就越多。

除了训练数据量之外,新数据集与原数据集的相似度也是一个需要考虑的点。倘若我们拥有较多的数据,而新数据集与原数据集的相似度却很低时,我们最好不要使用迁移学习进行预训练,而是从头开始训练整个网络。


为什么要迁移学习

关于使用迁移学习的原因,Andrew Ng没有提及,不多简单想来主要的原因主要有如下几点:

  1. 迁移学习可以弥补训练数据的不足。
  2. 迁移学习可以大大减少训练时间。
  3. 迁移学习(特别是同一领域相关的参数)可以有效地防止梯度下降卡在局部最优解处,一定程度上保证了算法的收敛性。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>迁移学习(Transfer Learning),又称预训练。即利用社区内开源的权重参数更快更好地训练自己的网络。然而,我一直纳闷的是,别人训练好 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>迁移学习(Transfer Learning),又称预训练。即利用社区内开源的权重参数更快更好地训练自己的网络。然而,我一直纳闷的是,别人训练好 @@ -747,13 +747,13 @@ 2020-01-27T10:51:46.000Z 2020-01-28T07:02:12.793Z -

之前在windows下面从来没想过权限的事情,而在ubuntu中这点就很受重视,可谓时时都会遇到权限管理的问题。即使如此,搞崩ubuntu的次数还是比windows要多的,不过好像心也没那么痛,可能ubuntu就是拿来玩的。虽然不是每次搞崩都是因为权限,但还是有必要理一理。

References

参考文献:
[1]完美应用ubuntu(第3版)


权限

在linux系统中,我们可以在终端使用ls -l查看目录下所有子目录和文件的权限属性。其输出结果中每一列的含义如下:

其实不做服务器的话没必要搞那么懂,我就讲一下我认为最重要的第一列。
首先,第一个字母表示的是文件类型,主要有下面几种:

其次,之后的九个字母三个为一组,分别表示的是文件所有者(u)的权限、同组用户(g)的权限和其他用户(o)的权限。这里属主一般就是sudo赋权进入的那个用户,一般在个人系统中就是特权用户root。另外,可以用“a”表示all users。
在每个三个字母组成的一组中,依次分别为读(r)、写(w)和执行(x)权限。若是字母,则表示可;若是“-”,则表示不可。例如“rw-”表示的是“可读可写不可执行”。
为了方便,还可以用数字代表权限:用4代表读权限,用2代表写权限,用1代表执行权限。可以发现,这样的三个数字之和(0-7)可以表示任何一种权限组合。
较为常用权限组合的有:


chmod

一般通过chmod命令来修改权限,主要有两种方法。

  1. 数字法

    这种方法最简洁,其基本格式是chmod (-R) 模式 文件名。这里的-R可以用来进行多级目录的权限设定,也就是将指定文件夹内的所有文件都修改权限。
    以两个较为常用的使用为例。

    1
    2
    3
    4
    5
    sudo chmod 666 文件名
    #赋予所有用户读和写的权限,一般没有权限时我都会使用这个命令

    sudo chmod 600 文件名
    #赋予文件所有者读和写的权限,给group和other只读权限
  2. 参数法

    这种方法适用于只需要改变单个用户的权限而又不想考虑或者计算别的用户的权限情况,其基本格式是chmod [u/g/o/a] [+/-/=] (rwxst) 文件名
    这里先解释一下几个重要的参数和符号。
    u:所属用户。
    g:同组用户。
    o:其他用户。
    a:所有用户,相当于ugo。
    +:原权限基础上增加权限。
    -:原权限基础上减少权限。
    =:无论原权限是什么,最后的权限都修改为这里指定的权限。
    r:不解释,不懂的话没好好看前文。
    w:不解释,不懂的话没好好看前文。
    x:不解释,不懂的话没好好看前文。
    s:运行时可置UID。
    t:运行时可置GID。
    来看例子:

    1
    2
    3
    4
    5
    6
    7
    8
    sudo chmod u+rw 文件名
    #给用户增加读写权限

    sudo chmod o-rwx 文件名
    #不允许其他用户读写执行

    sudo chmod g=rx 文件名
    #使同组用户只能读和执行
]]>
+

之前在windows下面从来没想过权限的事情,而在ubuntu中这点就很受重视,可谓时时都会遇到权限管理的问题。即使如此,搞崩ubuntu的次数还是比windows要多的,不过好像心也没那么痛,可能ubuntu就是拿来玩的。虽然不是每次搞崩都是因为权限,但还是有必要理一理。

References

参考文献:
[1]完美应用ubuntu(第3版)


权限

在linux系统中,我们可以在终端使用ls -l查看目录下所有子目录和文件的权限属性。其输出结果中每一列的含义如下:

其实不做服务器的话没必要搞那么懂,我就讲一下我认为最重要的第一列。
首先,第一个字母表示的是文件类型,主要有下面几种:

其次,之后的九个字母三个为一组,分别表示的是文件所有者(u)的权限、同组用户(g)的权限和其他用户(o)的权限。这里属主一般就是sudo赋权进入的那个用户,一般在个人系统中就是特权用户root。另外,可以用“a”表示all users。
在每个三个字母组成的一组中,依次分别为读(r)、写(w)和执行(x)权限。若是字母,则表示可;若是“-”,则表示不可。例如“rw-”表示的是“可读可写不可执行”。
为了方便,还可以用数字代表权限:用4代表读权限,用2代表写权限,用1代表执行权限。可以发现,这样的三个数字之和(0-7)可以表示任何一种权限组合。
较为常用权限组合的有:


chmod

一般通过chmod命令来修改权限,主要有两种方法。

  1. 数字法

    这种方法最简洁,其基本格式是chmod (-R) 模式 文件名。这里的-R可以用来进行多级目录的权限设定,也就是将指定文件夹内的所有文件都修改权限。
    以两个较为常用的使用为例。

    1
    2
    3
    4
    5
    sudo chmod 666 文件名
    #赋予所有用户读和写的权限,一般没有权限时我都会使用这个命令

    sudo chmod 600 文件名
    #赋予文件所有者读和写的权限,给group和other只读权限
  2. 参数法

    这种方法适用于只需要改变单个用户的权限而又不想考虑或者计算别的用户的权限情况,其基本格式是chmod [u/g/o/a] [+/-/=] (rwxst) 文件名
    这里先解释一下几个重要的参数和符号。
    u:所属用户。
    g:同组用户。
    o:其他用户。
    a:所有用户,相当于ugo。
    +:原权限基础上增加权限。
    -:原权限基础上减少权限。
    =:无论原权限是什么,最后的权限都修改为这里指定的权限。
    r:不解释,不懂的话没好好看前文。
    w:不解释,不懂的话没好好看前文。
    x:不解释,不懂的话没好好看前文。
    s:运行时可置UID。
    t:运行时可置GID。
    来看例子:

    1
    2
    3
    4
    5
    6
    7
    8
    sudo chmod u+rw 文件名
    #给用户增加读写权限

    sudo chmod o-rwx 文件名
    #不允许其他用户读写执行

    sudo chmod g=rx 文件名
    #使同组用户只能读和执行
]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>之前在windows下面从来没想过权限的事情,而在ubuntu中这点就很受重视,可谓时时都会遇到权限管理的问题。即使如此,搞崩ubuntu的次数 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前在windows下面从来没想过权限的事情,而在ubuntu中这点就很受重视,可谓时时都会遇到权限管理的问题。即使如此,搞崩ubuntu的次数 @@ -775,13 +775,13 @@ 2020-01-27T10:43:52.000Z 2020-01-27T10:48:35.416Z -

有时候会遇到文件无法重命名的问题,这里介绍一种很神奇的方法,亲测有效。


方法

  1. 首先,在终端把目录切到要重命名的文件目录下,或者直接在对应目录中打开终端。
  2. 接下来就是神奇的地方了,为了防止权限不够加个sudo赋个权。
    1
    sudo mv 原文件名 新文件名
]]>
+

有时候会遇到文件无法重命名的问题,这里介绍一种很神奇的方法,亲测有效。


方法

  1. 首先,在终端把目录切到要重命名的文件目录下,或者直接在对应目录中打开终端。
  2. 接下来就是神奇的地方了,为了防止权限不够加个sudo赋个权。
    1
    sudo mv 原文件名 新文件名
]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>有时候会遇到文件无法重命名的问题,这里介绍一种很神奇的方法,亲测有效。</p><hr><h1 id="方法"><a href="#方法" cla + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>有时候会遇到文件无法重命名的问题,这里介绍一种很神奇的方法,亲测有效。</p><hr><h1 id="方法"><a href="#方法" cla @@ -801,13 +801,13 @@ 2020-01-27T04:38:32.000Z 2020-01-27T10:34:14.733Z -

在我安装ubuntu18.04LTS的时候,由于下载语言包是真的久,我就翻了一下ubuntu安装界面的介绍,其中一开始就是对snap store的介绍。

这篇文章就说说snap和我之前遇到的问题。

References

电子文献:
https://www.jb51.net/article/128368.htm
https://blog.csdn.net/u011870280/article/details/80213866


snap

snap是ubuntu母公司Canonical于2016年4月发布ubuntu16.04时候引入的一种安全的、易于管理的、沙盒化的软件包格式,与传统的dpkg和apt有着很大的区别。在ubuntu软件中心下载安装的似乎都是snap管理的。这让一些商业闭源软件也能在linux上发布,说白了是ubuntu为了获得linux发行版霸权的一个重要举措,因此没少招黑。知乎上看到这么一句话,笑半天:“Fuck the political correct, make linux great again.(says 川·乌班图·普)”。


常用命令

  1. 列出已经安装的snap包

    1
    sudo snap list
  2. 搜索要安装的snap包

    1
    sudo snap find <text to search>
  3. 安装一个snap包

    1
    sudo snap install <snap name>
  4. 更新一个snap包

    1
    sudo snap refresh <snap name>

    注:如果后面不加包的名字就更新所有的snap包。

  5. 把一个包还原到以前安装的版本

    1
    sudo snap revert <snap name>
  6. 删除一个snap包

    1
    sudo snap remove <snap name>
  7. 查看最近的更改

    1
    snap changes
  8. 终止snap进程

    1
    sudo snap abort <进程序号>

后面两个命令将在下面的问题中发挥作用。


问题

当我在snap store也就是ubuntu软件中心下载pycharm和VScode时,遇到了如下报错:

1
snapd returned status code 409: Conflict

上网查找之后,才知道这个错误码409表示的是:由于和被请求的资源的当前状态之间存在冲突,请求无法完成。即并发执行时返回的错误码。
由于之前ubuntu软件中心无响应被我强制退出了,因此的确很有可能与之前进行到一半的安装冲突。于是使用snap changes查看最近的snap更改。

果然看见之前的snap进程依旧在“Doing”,因此根据对应的序号使用sudo snap abort终止进程。
这时再回到软件中心安装,就没有之前的报错了。
然而…


关于国内使用snap

因为网络原因,而且也没有可用的镜像,导致snapcraft在中国大陆地区访问速度非常非常慢,下载软件需要很长的时间并且很容易中途出错。
此外,由于snap软件会把主分区分成好多个loop,看起来真的不想说什么了。图源自贴吧,可以看到这挂载的snap软件包可以说是相当壮(别)观(扭)了。

还有一个杀死强迫症(比如我)的问题就是,snap会在家目录(即18.04的主文件夹)中创建一个snap文件夹,里面各种快捷方式、循环嵌套的文件夹,害…无法用语言描述,看了就知道,总之就是非常不爽。主要是一些资料、文件一般也会放在家目录下面,看到了snap在那边亮眼睛真的难受。
实在不知道为什么社区里有不少人推崇snap(不过国外没速度限制,snap对他们来说挺方便的)。
总而言之,综合速度(硬伤)和美观舒适度考虑,还是尽量避免使用snap命令安装软件,也不要下载ubuntu软件商店中的snap格式的软件包(基本都是)。甚至有些“安装ubuntu之后必做的…件事”等诸如此类的ubuntu配置或者美化的教程内直接把卸载snap列作其中一项哈哈。
总之管理软件还是apt优先,详见我的博文ubuntu笔记:apt包管理以及如何更新软件列表

]]>
+

在我安装ubuntu18.04LTS的时候,由于下载语言包是真的久,我就翻了一下ubuntu安装界面的介绍,其中一开始就是对snap store的介绍。

这篇文章就说说snap和我之前遇到的问题。

References

电子文献:
https://www.jb51.net/article/128368.htm
https://blog.csdn.net/u011870280/article/details/80213866


snap

snap是ubuntu母公司Canonical于2016年4月发布ubuntu16.04时候引入的一种安全的、易于管理的、沙盒化的软件包格式,与传统的dpkg和apt有着很大的区别。在ubuntu软件中心下载安装的似乎都是snap管理的。这让一些商业闭源软件也能在linux上发布,说白了是ubuntu为了获得linux发行版霸权的一个重要举措,因此没少招黑。知乎上看到这么一句话,笑半天:“Fuck the political correct, make linux great again.(says 川·乌班图·普)”。


常用命令

  1. 列出已经安装的snap包

    1
    sudo snap list
  2. 搜索要安装的snap包

    1
    sudo snap find <text to search>
  3. 安装一个snap包

    1
    sudo snap install <snap name>
  4. 更新一个snap包

    1
    sudo snap refresh <snap name>

    注:如果后面不加包的名字就更新所有的snap包。

  5. 把一个包还原到以前安装的版本

    1
    sudo snap revert <snap name>
  6. 删除一个snap包

    1
    sudo snap remove <snap name>
  7. 查看最近的更改

    1
    snap changes
  8. 终止snap进程

    1
    sudo snap abort <进程序号>

后面两个命令将在下面的问题中发挥作用。


问题

当我在snap store也就是ubuntu软件中心下载pycharm和VScode时,遇到了如下报错:

1
snapd returned status code 409: Conflict

上网查找之后,才知道这个错误码409表示的是:由于和被请求的资源的当前状态之间存在冲突,请求无法完成。即并发执行时返回的错误码。
由于之前ubuntu软件中心无响应被我强制退出了,因此的确很有可能与之前进行到一半的安装冲突。于是使用snap changes查看最近的snap更改。

果然看见之前的snap进程依旧在“Doing”,因此根据对应的序号使用sudo snap abort终止进程。
这时再回到软件中心安装,就没有之前的报错了。
然而…


关于国内使用snap

因为网络原因,而且也没有可用的镜像,导致snapcraft在中国大陆地区访问速度非常非常慢,下载软件需要很长的时间并且很容易中途出错。
此外,由于snap软件会把主分区分成好多个loop,看起来真的不想说什么了。图源自贴吧,可以看到这挂载的snap软件包可以说是相当壮(别)观(扭)了。

还有一个杀死强迫症(比如我)的问题就是,snap会在家目录(即18.04的主文件夹)中创建一个snap文件夹,里面各种快捷方式、循环嵌套的文件夹,害…无法用语言描述,看了就知道,总之就是非常不爽。主要是一些资料、文件一般也会放在家目录下面,看到了snap在那边亮眼睛真的难受。
实在不知道为什么社区里有不少人推崇snap(不过国外没速度限制,snap对他们来说挺方便的)。
总而言之,综合速度(硬伤)和美观舒适度考虑,还是尽量避免使用snap命令安装软件,也不要下载ubuntu软件商店中的snap格式的软件包(基本都是)。甚至有些“安装ubuntu之后必做的…件事”等诸如此类的ubuntu配置或者美化的教程内直接把卸载snap列作其中一项哈哈。
总之管理软件还是apt优先,详见我的博文ubuntu笔记:apt包管理以及如何更新软件列表

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>在我安装ubuntu18.04LTS的时候,由于下载语言包是真的久,我就翻了一下ubuntu安装界面的介绍,其中一开始就是对snap store + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在我安装ubuntu18.04LTS的时候,由于下载语言包是真的久,我就翻了一下ubuntu安装界面的介绍,其中一开始就是对snap store @@ -829,13 +829,13 @@ 2020-01-26T00:34:48.000Z 2020-01-26T02:12:38.151Z -

似乎是为了支持由武汉深之度科技开发的国产linux系统Deepin,近年来许多常用软件都提供了linux客户端,比如QQ for linux,baidunetdisk for linux。然而我安装百度网盘后发现打不开,一打开就报错,后来才知道百度网盘仅支持ubuntu18之后的版本。于是就又涉及到deb包的卸载问题了。

References

电子文献:
https://askubuntu.com/questions/18804/what-do-the-various-dpkg-flags-like-ii-rc-mean
https://blog.csdn.net/sun2333/article/details/82707362


dpkg flag

我们可以使用dpkg -l | grep '软件名'来查看相应软件的安装状态,这时一般会出现有两个字母组成的一个flag。具体可以看后文中的截图。这里我想先整理一下这两个字母的含义。

  1. 第一个字母:所需的状态desired package state(”selection state”)

    • u——未知unknown
    • i——安装install
    • r——删除/卸载remove/deinstall
    • p——清除(除包含配置文件)purge(remove including config files)
    • h——保持hold
  2. 第二个字母:当前包状态current package state

    • n——未安装not-installed
    • i——已安装installed
    • c——仅安装配置文件config-files(only the config files are installed)
    • U——解包unpacked
    • F——由于某种原因配置失败half-configured(configuration failed for some reason)
    • h——由于某种原因安装失败half-installed(installation failed for some reason)
    • W——包正在等待来自另一个包的触发器triggers-awaited(package is waiting for a trigger from another package)
    • t——包已被触发triggers-pending(package has been triggered)
  3. 第三个字母:错误状态error state

    第三个字母通常情况下是一个空格,一般不会看到。
    • R——包破损,需要重新安装reinst-required(package broken, reinstallation required)

安装

使用如下命令进行安装。

1
sudo dpkg -i package-file-name

这里的-i表示的是install。注意,这里的package-file-name包括后缀如“.deb”。


卸载

下面这张图就是我卸载的过程。

首先我使用了dpkg -l | grep '软件名'命令来查看我系统上百度网盘的安装状态。结果显示为“ii”,表示“installed ok installed”即它应该被安装并且已安装。
随后,利用-r参数,使用下面命令进行移除。

1
sudo dpkg -r 软件名

注意,这里的软件名不需要添加引号。
移除之后,我们可以再次使用dpkg -l | grep '软件名'来查看百度网盘的安装状态。结果显示为“rc”,表示“removed ok config-files”即它已经被移除/卸载,但它的配置文件仍然存在。
这时我们也是使用如下命令来彻底卸载软件包(包括配置文件)。

1
sudo dpkg -P 软件名

ubuntu笔记:释放空间一文中,有一次性清理所有残留配置文件的方法,可以看一下。

]]>
+

似乎是为了支持由武汉深之度科技开发的国产linux系统Deepin,近年来许多常用软件都提供了linux客户端,比如QQ for linux,baidunetdisk for linux。然而我安装百度网盘后发现打不开,一打开就报错,后来才知道百度网盘仅支持ubuntu18之后的版本。于是就又涉及到deb包的卸载问题了。

References

电子文献:
https://askubuntu.com/questions/18804/what-do-the-various-dpkg-flags-like-ii-rc-mean
https://blog.csdn.net/sun2333/article/details/82707362


dpkg flag

我们可以使用dpkg -l | grep '软件名'来查看相应软件的安装状态,这时一般会出现有两个字母组成的一个flag。具体可以看后文中的截图。这里我想先整理一下这两个字母的含义。

  1. 第一个字母:所需的状态desired package state(”selection state”)

    • u——未知unknown
    • i——安装install
    • r——删除/卸载remove/deinstall
    • p——清除(除包含配置文件)purge(remove including config files)
    • h——保持hold
  2. 第二个字母:当前包状态current package state

    • n——未安装not-installed
    • i——已安装installed
    • c——仅安装配置文件config-files(only the config files are installed)
    • U——解包unpacked
    • F——由于某种原因配置失败half-configured(configuration failed for some reason)
    • h——由于某种原因安装失败half-installed(installation failed for some reason)
    • W——包正在等待来自另一个包的触发器triggers-awaited(package is waiting for a trigger from another package)
    • t——包已被触发triggers-pending(package has been triggered)
  3. 第三个字母:错误状态error state

    第三个字母通常情况下是一个空格,一般不会看到。
    • R——包破损,需要重新安装reinst-required(package broken, reinstallation required)

安装

使用如下命令进行安装。

1
sudo dpkg -i package-file-name

这里的-i表示的是install。注意,这里的package-file-name包括后缀如“.deb”。


卸载

下面这张图就是我卸载的过程。

首先我使用了dpkg -l | grep '软件名'命令来查看我系统上百度网盘的安装状态。结果显示为“ii”,表示“installed ok installed”即它应该被安装并且已安装。
随后,利用-r参数,使用下面命令进行移除。

1
sudo dpkg -r 软件名

注意,这里的软件名不需要添加引号。
移除之后,我们可以再次使用dpkg -l | grep '软件名'来查看百度网盘的安装状态。结果显示为“rc”,表示“removed ok config-files”即它已经被移除/卸载,但它的配置文件仍然存在。
这时我们也是使用如下命令来彻底卸载软件包(包括配置文件)。

1
sudo dpkg -P 软件名

ubuntu笔记:释放空间一文中,有一次性清理所有残留配置文件的方法,可以看一下。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>似乎是为了支持由武汉深之度科技开发的国产linux系统Deepin,近年来许多常用软件都提供了linux客户端,比如QQ for linux,b + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>似乎是为了支持由武汉深之度科技开发的国产linux系统Deepin,近年来许多常用软件都提供了linux客户端,比如QQ for linux,b @@ -857,13 +857,13 @@ 2020-01-23T02:39:54.000Z 2020-01-30T11:53:20.261Z -

今天ubuntu系统又双叒叕被我搞崩了,折腾一个大半天之后还是无解,没办法只好根据之前博文ubuntu笔记:重装ubuntu——记一段辛酸血泪史中的方法重装系统。心里还是非常庆幸还好当初留心写了一下。
痛定思痛,由于之前没有系统地学习linux操作系统,鸟哥的书也就看了一部分,因此觉得自己以后应该更加谨慎小心一些,每一步命令都要看明白再执行,不然再翻车的话真的要心态爆炸的。
之前在markdown笔记:markdown的基本使用中介绍过typora,这里主要是以它为例,仔细地分析一下安装软件时每一步命令的作用。

References

电子文献:
https://support.typora.io/Typora-on-Linux/


安装过程

  1. 信任软件包密匙

    1
    2
    sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA300B7755AFCFAE
    #optional, but recommended

    这条命令应该就是添加新的密匙并信任,一般在配置apt-get源之前运行。
    apt-key的描述如下:“apt-key is used to manage the list of keys used by apt to authenticate packages. Packages which have been authenticated using these keys will be considered trusted.”由于每个发布的Debian软件包都是通过密钥认证的,而apt-key命令正是用来管理Debian软件包密钥的。

  2. 添加软件库

    由于默认的软件仓库里是没有typora的,所以要添加对应的软件仓库。

    1
    2
    sudo add-apt-repository 'deb https://typora.io/linux ./'
    #add Typora's repository
  3. 更新软件列表

    在添加了新的软件仓库之后,我们需要更新软件列表使得后面的操作能找到对应的软件包。

    1
    sudo apt-get update
  4. 安装

    更新apt-get之后,就可以安装前面添加的库中的软件包了。

    1
    2
    sudo apt-get install typora
    #install typora
  5. 有软件包无法下载

    在install的过程中,提示我:“有几个软件包无法下载”。
    于是我照着提示执行了下面的命令:

    1
    sudo apt-get update --fix-missing

    然后再sudo apt-get install typora,就可以了。
    如果还是有问题的话,可能需要更换软件源,换成国内的镜像比较好。

  6. 更新

    安装后的typora由apt-get管理,因此可以用以下命令来更新软件包。
    1
    sudo apt-get upgrade

软连接

痛定思痛,还是决定把这回翻车的地方写一下。
本来用命令行打开matlab挺好的,我自作自受想转个matlab-support想着用图标打开,结果报错:MATLAB is selecting SOFTWARE OPENGL rendering。到网上查资料后找到一个貌似可行的方法。
根据他所说,这是因为matlab的libstdc++库和系统库不匹配造成的,所以需要用如下命令建立一个连接。

1
ln -s /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21  /usr/local/MATLAB/R2015b/sys/os/glnxa64/libstdc++.so.6

注意,这里是R2015b。由于我下载的是R2018b,显然这里的地址是不一样的,当时比较心急直接回车了。结果还是没有解决问题。
这句命令其实就是建立一个软连接,其基本格式是ln –s 源文件目录 目标文件目录。它只会在选定的位置上生成一个文件的镜像,不会占用磁盘空间,类似于windows中的快捷方式。若没加-s,就是硬链接,即会在选定的位置上生成一个和源文件大小相同的文件。不过,无论是软链接还是硬链接,文件都保持同步变化。

讲道理即使目录出错也是不会有问题的,然而当我再次开机尝试进入系统时,就出现了卡在recovering journal的情况。
卡住的位置仅有两行,第一行是recovering journal,第二行我在ubuntu社区里找到了一个比较类似的,如下图所示。

他后面解答的方法如下。

可以试一试,我也照着做下来了,但是没起作用。
我也在网上看了其它的一些办法,有先进入recovery mode然后选择resume normal boot就好了的(就是返回正常启动,很玄学),然而我没用;也有check all file systems的,我也尝试了but failed。

]]>
+

今天ubuntu系统又双叒叕被我搞崩了,折腾一个大半天之后还是无解,没办法只好根据之前博文ubuntu笔记:重装ubuntu——记一段辛酸血泪史中的方法重装系统。心里还是非常庆幸还好当初留心写了一下。
痛定思痛,由于之前没有系统地学习linux操作系统,鸟哥的书也就看了一部分,因此觉得自己以后应该更加谨慎小心一些,每一步命令都要看明白再执行,不然再翻车的话真的要心态爆炸的。
之前在markdown笔记:markdown的基本使用中介绍过typora,这里主要是以它为例,仔细地分析一下安装软件时每一步命令的作用。

References

电子文献:
https://support.typora.io/Typora-on-Linux/


安装过程

  1. 信任软件包密匙

    1
    2
    sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA300B7755AFCFAE
    #optional, but recommended

    这条命令应该就是添加新的密匙并信任,一般在配置apt-get源之前运行。
    apt-key的描述如下:“apt-key is used to manage the list of keys used by apt to authenticate packages. Packages which have been authenticated using these keys will be considered trusted.”由于每个发布的Debian软件包都是通过密钥认证的,而apt-key命令正是用来管理Debian软件包密钥的。

  2. 添加软件库

    由于默认的软件仓库里是没有typora的,所以要添加对应的软件仓库。

    1
    2
    sudo add-apt-repository 'deb https://typora.io/linux ./'
    #add Typora's repository
  3. 更新软件列表

    在添加了新的软件仓库之后,我们需要更新软件列表使得后面的操作能找到对应的软件包。

    1
    sudo apt-get update
  4. 安装

    更新apt-get之后,就可以安装前面添加的库中的软件包了。

    1
    2
    sudo apt-get install typora
    #install typora
  5. 有软件包无法下载

    在install的过程中,提示我:“有几个软件包无法下载”。
    于是我照着提示执行了下面的命令:

    1
    sudo apt-get update --fix-missing

    然后再sudo apt-get install typora,就可以了。
    如果还是有问题的话,可能需要更换软件源,换成国内的镜像比较好。

  6. 更新

    安装后的typora由apt-get管理,因此可以用以下命令来更新软件包。
    1
    sudo apt-get upgrade

软连接

痛定思痛,还是决定把这回翻车的地方写一下。
本来用命令行打开matlab挺好的,我自作自受想转个matlab-support想着用图标打开,结果报错:MATLAB is selecting SOFTWARE OPENGL rendering。到网上查资料后找到一个貌似可行的方法。
根据他所说,这是因为matlab的libstdc++库和系统库不匹配造成的,所以需要用如下命令建立一个连接。

1
ln -s /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21  /usr/local/MATLAB/R2015b/sys/os/glnxa64/libstdc++.so.6

注意,这里是R2015b。由于我下载的是R2018b,显然这里的地址是不一样的,当时比较心急直接回车了。结果还是没有解决问题。
这句命令其实就是建立一个软连接,其基本格式是ln –s 源文件目录 目标文件目录。它只会在选定的位置上生成一个文件的镜像,不会占用磁盘空间,类似于windows中的快捷方式。若没加-s,就是硬链接,即会在选定的位置上生成一个和源文件大小相同的文件。不过,无论是软链接还是硬链接,文件都保持同步变化。

讲道理即使目录出错也是不会有问题的,然而当我再次开机尝试进入系统时,就出现了卡在recovering journal的情况。
卡住的位置仅有两行,第一行是recovering journal,第二行我在ubuntu社区里找到了一个比较类似的,如下图所示。

他后面解答的方法如下。

可以试一试,我也照着做下来了,但是没起作用。
我也在网上看了其它的一些办法,有先进入recovery mode然后选择resume normal boot就好了的(就是返回正常启动,很玄学),然而我没用;也有check all file systems的,我也尝试了but failed。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>今天ubuntu系统又双叒叕被我搞崩了,折腾一个大半天之后还是无解,没办法只好根据之前博文<a href="https://gsy00517.g + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>今天ubuntu系统又双叒叕被我搞崩了,折腾一个大半天之后还是无解,没办法只好根据之前博文<a href="https://gsy00517.g @@ -889,13 +889,13 @@ 2020-01-22T08:45:03.000Z 2020-02-02T06:59:57.882Z -

以前觉得深度学习就是有很多层的神经网络,或者周志华提出的深度随机森林,总之只要是有很“深”的结构就是深度学习。直到不久前一位计科大佬告诉我深度学习是end-to-end(也表示成“e2e”)的,当时听的也是一知半解,回去查了一下后终于恍然大悟。本文主要基于Andrew Ng的课程中“What is end-to-end deep learning?”和“Whether to use end-to-end learning?”两节。推荐可以去看一看,讲得可以说是很浅显易懂了。


什么是end-to-end learning

传统机器学习的流程往往由多个独立的模块组成,比如在一个典型的自然语言处理(Natural Language Processing)问题中,包括分词、词性标注、句法分析、语义分析等多个独立步骤,每个步骤是一个独立的任务,其结果的好坏会影响到下一步骤,从而影响整个训练的结果,这就是非端到端的。
而深度学习模型在训练过程中,从输入端(输入数据)到输出端得到一个预测结果,该结果与真实结果相比较会得到一个误差,这个误差将用于模型每一层的调整(比如反向传播),这种训练直到模型收敛或达到预期的效果才结束,这就是端到端(end-to-end)的。

相比传统方法每一个模块都有较为明确的输出,端到端的深度学习更像是一个神秘的整体。通俗的说,端到端的深度学习能够让“数据说话”。不过这种方法是很吃数据的,因此还不至于在每个领域都胜过甚至代替传统的机器学习方法。
由于以前被标注的数据没有那么丰富,因此经典机器学习方法始终占据主流。随着近年来一个又一个数据集的出现,这种状况发生了转变。一个重要的转折点就是AlexNet的横空出世,详见deep-learning笔记:开启深度学习热潮——AlexNet
在目标跟踪领域,继相关滤波大火之后,也出现了很多优秀的深度学习算法。其中,孪生网络充分借鉴了两者的优势,取得了不错的成绩。

上面是SiameseFC的主体架构,它借用了神经网络去提取特征,而不是利用一些较为经典的特征。实际上,也可以认为它是端到端的,在调整了相关滤波的形式之后,使相关滤波的操作过程可求导,从而实现了整个模型内部的前向传播和反向传播,实现端到端。
那么,这样做有什么意义呢?误差理论告诉,误差传播的途径本身会导致误差的累积,多个阶段大概率会导致误差累积,而端到端的训练就能减少误差传播的途径,实现联合优化。


何时该用end-to-end learning

相比之下,端到端学习省去了每一步中间的数据处理和每一步模型的设计(这往往会涉及相当多的专业知识),但是端到端学习也有两个重要的缺点。

  1. 缺点一:需要大量的数据

    Andrew Ng在视频课程中举了一个例子:百度的门禁系统可以识别靠近的人脸并放行。
    如果直接使用端到端学习,那么需要训练的数据集就是一系列照片或者视频,其中人会随机出现在任何位置、任何距离等等,而这样标注好的数据集是很匮乏的。
    但是,如果我们把这个任务拆解成两个子任务。首先,在照片或者视频中定位人脸,然后放大(使人脸居中等);其次,对放大好的人脸再进行检验。这两种任务都有非常丰富的数据集或者方法可供使用。实际上,我觉得可以应用两个端到端的模型来解决这两个问题,但合起来就不是端到端的了。但在目前现有数据量的情况下,这依然能比直接端到端的方法表现得好。
  2. 缺点二:可能排除有用的人工设计

    前面提到,人工设计的模块往往是基于知识的。而知识的注入有时候会大大简化模型(尤其是数据不足的时候)。这里Andrew Ng又举了一个例子:通过X光片来估计年龄。
    传统的方法就是照一张图片,然后分割出每一块骨头并测量长度,然后通过这些长度结合理论和统计来估计年龄。
    而若是使用端到端的模型,就是直接建立图片与年龄之间的联系,这显然是很难且很复杂的,训练结果的表现也可想而知。

端到端学习确实在很多领域都能取得state-of-the-art的表现,但何时使用还是要具体问题具体分析。


神经网络学到了什么

我在前文中写了这样一句话:“相比传统方法每一个模块都有较为明确的输出,端到端的深度学习更像是一个神秘的整体”。但实际上一些研究者通过分离观察每一层,发现e2e的神经网络的确还是学到了一点东西的。
一般而言,神经网络前几层学到的内容包含的信息比较丰富具体。越到后面越抽象,即越到后面包含的语义信息越多。下面是对每一个卷积核(神经元)做可视化处理,左图为靠前的某层的可视化结果,而右图为靠后的某层的可视化结果。可以看到,相较于后面的层,前几层的卷积核似乎呈现出更明确的任务或者说功能。它们通常会找一些简单的特征,比如说边缘或者颜色阴影。

我们可以对第一层卷积层做特征可视化来看一下。

从特征可视化结果中看,出第一层卷积提取出了不同的特征,有些突出了斑马的形状,有些突出了背景,有些突出了斑马的斑纹等。
下面是Andrew Ng在课程中举得一个可视化例子,他所采用的方法是对每层中的隐藏单元用数据集去遍历,并且寻找出9个使得隐藏单元有较大的输出或是较大的激活的图片或者图像块。注意网络层数越深其感受野会越大。详见deep-learning笔记:着眼于深度——VGG简介与pytorch实现

实际上,实现可视化的方法有多种,可以利用反卷积,也可以对一系列图像求响应值,上面的两个例子就是用了不同的方法(前者是针对一张图,而Andrew Ng用了一个数据集)。在Visualizing and Understanding Convolutional Networks一文中,作者也提出了一些更复杂的方式来可视化卷积神经网络的计算。我在computer-vision笔记:上采样和下采样中,也对反卷积进行了介绍。
在NLP领域,也有类似的发现,比如一些训练过后的神经元对特定标点的响应特别强烈,而有一些训练过后的神经元对一些特定语气词的响应特别强烈。

]]>
+

以前觉得深度学习就是有很多层的神经网络,或者周志华提出的深度随机森林,总之只要是有很“深”的结构就是深度学习。直到不久前一位计科大佬告诉我深度学习是end-to-end(也表示成“e2e”)的,当时听的也是一知半解,回去查了一下后终于恍然大悟。本文主要基于Andrew Ng的课程中“What is end-to-end deep learning?”和“Whether to use end-to-end learning?”两节。推荐可以去看一看,讲得可以说是很浅显易懂了。


什么是end-to-end learning

传统机器学习的流程往往由多个独立的模块组成,比如在一个典型的自然语言处理(Natural Language Processing)问题中,包括分词、词性标注、句法分析、语义分析等多个独立步骤,每个步骤是一个独立的任务,其结果的好坏会影响到下一步骤,从而影响整个训练的结果,这就是非端到端的。
而深度学习模型在训练过程中,从输入端(输入数据)到输出端得到一个预测结果,该结果与真实结果相比较会得到一个误差,这个误差将用于模型每一层的调整(比如反向传播),这种训练直到模型收敛或达到预期的效果才结束,这就是端到端(end-to-end)的。

相比传统方法每一个模块都有较为明确的输出,端到端的深度学习更像是一个神秘的整体。通俗的说,端到端的深度学习能够让“数据说话”。不过这种方法是很吃数据的,因此还不至于在每个领域都胜过甚至代替传统的机器学习方法。
由于以前被标注的数据没有那么丰富,因此经典机器学习方法始终占据主流。随着近年来一个又一个数据集的出现,这种状况发生了转变。一个重要的转折点就是AlexNet的横空出世,详见deep-learning笔记:开启深度学习热潮——AlexNet
在目标跟踪领域,继相关滤波大火之后,也出现了很多优秀的深度学习算法。其中,孪生网络充分借鉴了两者的优势,取得了不错的成绩。

上面是SiameseFC的主体架构,它借用了神经网络去提取特征,而不是利用一些较为经典的特征。实际上,也可以认为它是端到端的,在调整了相关滤波的形式之后,使相关滤波的操作过程可求导,从而实现了整个模型内部的前向传播和反向传播,实现端到端。
那么,这样做有什么意义呢?误差理论告诉,误差传播的途径本身会导致误差的累积,多个阶段大概率会导致误差累积,而端到端的训练就能减少误差传播的途径,实现联合优化。


何时该用end-to-end learning

相比之下,端到端学习省去了每一步中间的数据处理和每一步模型的设计(这往往会涉及相当多的专业知识),但是端到端学习也有两个重要的缺点。

  1. 缺点一:需要大量的数据

    Andrew Ng在视频课程中举了一个例子:百度的门禁系统可以识别靠近的人脸并放行。
    如果直接使用端到端学习,那么需要训练的数据集就是一系列照片或者视频,其中人会随机出现在任何位置、任何距离等等,而这样标注好的数据集是很匮乏的。
    但是,如果我们把这个任务拆解成两个子任务。首先,在照片或者视频中定位人脸,然后放大(使人脸居中等);其次,对放大好的人脸再进行检验。这两种任务都有非常丰富的数据集或者方法可供使用。实际上,我觉得可以应用两个端到端的模型来解决这两个问题,但合起来就不是端到端的了。但在目前现有数据量的情况下,这依然能比直接端到端的方法表现得好。
  2. 缺点二:可能排除有用的人工设计

    前面提到,人工设计的模块往往是基于知识的。而知识的注入有时候会大大简化模型(尤其是数据不足的时候)。这里Andrew Ng又举了一个例子:通过X光片来估计年龄。
    传统的方法就是照一张图片,然后分割出每一块骨头并测量长度,然后通过这些长度结合理论和统计来估计年龄。
    而若是使用端到端的模型,就是直接建立图片与年龄之间的联系,这显然是很难且很复杂的,训练结果的表现也可想而知。

端到端学习确实在很多领域都能取得state-of-the-art的表现,但何时使用还是要具体问题具体分析。


神经网络学到了什么

我在前文中写了这样一句话:“相比传统方法每一个模块都有较为明确的输出,端到端的深度学习更像是一个神秘的整体”。但实际上一些研究者通过分离观察每一层,发现e2e的神经网络的确还是学到了一点东西的。
一般而言,神经网络前几层学到的内容包含的信息比较丰富具体。越到后面越抽象,即越到后面包含的语义信息越多。下面是对每一个卷积核(神经元)做可视化处理,左图为靠前的某层的可视化结果,而右图为靠后的某层的可视化结果。可以看到,相较于后面的层,前几层的卷积核似乎呈现出更明确的任务或者说功能。它们通常会找一些简单的特征,比如说边缘或者颜色阴影。

我们可以对第一层卷积层做特征可视化来看一下。

从特征可视化结果中看,出第一层卷积提取出了不同的特征,有些突出了斑马的形状,有些突出了背景,有些突出了斑马的斑纹等。
下面是Andrew Ng在课程中举得一个可视化例子,他所采用的方法是对每层中的隐藏单元用数据集去遍历,并且寻找出9个使得隐藏单元有较大的输出或是较大的激活的图片或者图像块。注意网络层数越深其感受野会越大。详见deep-learning笔记:着眼于深度——VGG简介与pytorch实现

实际上,实现可视化的方法有多种,可以利用反卷积,也可以对一系列图像求响应值,上面的两个例子就是用了不同的方法(前者是针对一张图,而Andrew Ng用了一个数据集)。在Visualizing and Understanding Convolutional Networks一文中,作者也提出了一些更复杂的方式来可视化卷积神经网络的计算。我在computer-vision笔记:上采样和下采样中,也对反卷积进行了介绍。
在NLP领域,也有类似的发现,比如一些训练过后的神经元对特定标点的响应特别强烈,而有一些训练过后的神经元对一些特定语气词的响应特别强烈。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>以前觉得深度学习就是有很多层的神经网络,或者周志华提出的深度随机森林,总之只要是有很“深”的结构就是深度学习。直到不久前一位计科大佬告诉我深度学 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>以前觉得深度学习就是有很多层的神经网络,或者周志华提出的深度随机森林,总之只要是有很“深”的结构就是深度学习。直到不久前一位计科大佬告诉我深度学 @@ -915,13 +915,13 @@ 2020-01-21T11:47:51.000Z 2020-01-28T01:56:53.849Z -

之前在matlab笔记:安装MinGW编译器一文中已经介绍过,MEX文件函数是Matlab提供的一种混合编程方式。通过MEX,用户可以在matlab中调用C、C++(没有C#,但我想提一下其实C#的真正含义是C++++,因为#其实就是四个+)或者Fortran编写的计算程序,加速matlab内部的矩阵运算(尤其是加速matlab代码中的for循环)。mex本质上是一个动态链接库文件(dll),可以被matlab动态加载并执行。然而在使用的过程中,我又碰到了许多问题。

References

电子文献:
https://ww2.mathworks.cn/help/matlab/call-mex-file-functions.html
https://blog.csdn.net/hijack00/article/details/52228253
https://jingyan.baidu.com/article/3a2f7c2ea00a9c66aed61163.html


安装版本适配的MinGW编译器

根据之前文章中写的配置方法,在编译MEX的时候,虽然没有问题,但是却出现了警告:使用的是不受支持的MinGW编译器版本。
于是我先查看了当前使用的编译器的版本。方法如下:

  1. 在MinGW-w64编译器的安装目录中,找到gcc.exe可执行文件的存在位置。
  2. 打开命令行,切换到刚刚找到的gcc.exe文件所在的目录。
  3. 键入gcc -v即可查看当前编译器的版本。

这时我使用的是5.1.0版本,于是我又到mathworks的网站上看了一下各个matlab版本适配的编译器版本。

这里我把图片截过来了,就不用去找了。我使用的是matlab R2019a,因此适配的是MinGW GCC 6.3(似乎高一点或者低一点都不行)。
于是我根据它所提供的SourceForge网址去找新版本的安装包,下载解压之后是一个不含任何可执行文件(exe)的文件夹,而且是7.0.0版本的,无法自主地选择。
寻找良久之后,我终于发现了一个在线安装文件,也建议下载这个,因为后面需要选择特定版本来安装。

下载完成后直接双击安装,这里会有一个安装设置界面。这个要注意一下,别点过去了。版本号一定要设置成对应的,比如我是6.3.0;另外,由于安装在windows 64位系统上,所以选择x86_64以及win32;至于其它的选项可以任选,一般默认就好了。

之后就是一路“下一步”,记得记住安装路径。
之后就是用和matlab笔记:安装MinGW编译器中所写的相同的方式添加环境变量。可以直接把之前已有的MW_MINGW64_LOC的值替换成刚刚记下的路径,最后别忘了在matlab中setenv
这时也可以把之前的编译器删了,如果是TDM-GCC的话那很方便,直接在它的一个管理界面中uninstall就行了,另外还会剩下一个空文件夹,手动删除就行。


连接外部库

在我使用的过程中,我还遇到了如下ERROR: Unable to compile MEX function: “MEX 找不到使用 -l 选项指定的库 ‘ut’。

上网搜了一下后,我才知道MEX命令可以用-L选项指定第三方库的路径,用-l来连接第三方库文件。值得注意的是,这里使用的是该库文件的文件名,不包含其文件扩展名。其基本格式如下:

1
mex -L<library_path> -l<library>

可以发现,-L<library_path>-l<library>之间是没有加空格的。
可是看了这些,我还是解决不了我的问题。
这里说一下我出现这个问题的背景,最近接触计算机视觉中的目标跟踪这一块,正在学习vot-toolkit的使用。
我既问了度娘又问了谷哥,可是没有看到任何这个问题及其解决方法。于是我缩小范围,看了看Github上vot-toolkit的issues和VOT Challenge technical support的Google groups,惊喜的是的确都找到了同样的问题。


然而都只有问题没有解答。无奈,还是自己想办法吧。
其实跟着报错的提示来修改并不难,关键是要找到该修改哪里。
由于报错提示的函数中根本没有MEX连接库文件的指令(我一行一行代码找的),于是我想能不能找到MEX的编译文件。最终我在vot-toolkit-master\utilities中找到了一个名为compile_mex.m的matlab文件,其中有这样的一串代码。

1
2
3
4
5
if is_octave()
arguments{end+1} = '-DOCTAVE';
else
arguments{end+1} = '-lut';
end

我用的是matlab,不是octave(后者相当于轻量级的免费matlab,语法什么的基本一致),那么执行的应该是else后面的语句,而在这里可以看到调用ut的命令。
于是我就把这里的-lut改成了-Lut试了一下,果然成功了。
其实用有搜索功能的IDE的话或许能够更快地解决这个问题。

]]>
+

之前在matlab笔记:安装MinGW编译器一文中已经介绍过,MEX文件函数是Matlab提供的一种混合编程方式。通过MEX,用户可以在matlab中调用C、C++(没有C#,但我想提一下其实C#的真正含义是C++++,因为#其实就是四个+)或者Fortran编写的计算程序,加速matlab内部的矩阵运算(尤其是加速matlab代码中的for循环)。mex本质上是一个动态链接库文件(dll),可以被matlab动态加载并执行。然而在使用的过程中,我又碰到了许多问题。

References

电子文献:
https://ww2.mathworks.cn/help/matlab/call-mex-file-functions.html
https://blog.csdn.net/hijack00/article/details/52228253
https://jingyan.baidu.com/article/3a2f7c2ea00a9c66aed61163.html


安装版本适配的MinGW编译器

根据之前文章中写的配置方法,在编译MEX的时候,虽然没有问题,但是却出现了警告:使用的是不受支持的MinGW编译器版本。
于是我先查看了当前使用的编译器的版本。方法如下:

  1. 在MinGW-w64编译器的安装目录中,找到gcc.exe可执行文件的存在位置。
  2. 打开命令行,切换到刚刚找到的gcc.exe文件所在的目录。
  3. 键入gcc -v即可查看当前编译器的版本。

这时我使用的是5.1.0版本,于是我又到mathworks的网站上看了一下各个matlab版本适配的编译器版本。

这里我把图片截过来了,就不用去找了。我使用的是matlab R2019a,因此适配的是MinGW GCC 6.3(似乎高一点或者低一点都不行)。
于是我根据它所提供的SourceForge网址去找新版本的安装包,下载解压之后是一个不含任何可执行文件(exe)的文件夹,而且是7.0.0版本的,无法自主地选择。
寻找良久之后,我终于发现了一个在线安装文件,也建议下载这个,因为后面需要选择特定版本来安装。

下载完成后直接双击安装,这里会有一个安装设置界面。这个要注意一下,别点过去了。版本号一定要设置成对应的,比如我是6.3.0;另外,由于安装在windows 64位系统上,所以选择x86_64以及win32;至于其它的选项可以任选,一般默认就好了。

之后就是一路“下一步”,记得记住安装路径。
之后就是用和matlab笔记:安装MinGW编译器中所写的相同的方式添加环境变量。可以直接把之前已有的MW_MINGW64_LOC的值替换成刚刚记下的路径,最后别忘了在matlab中setenv
这时也可以把之前的编译器删了,如果是TDM-GCC的话那很方便,直接在它的一个管理界面中uninstall就行了,另外还会剩下一个空文件夹,手动删除就行。


连接外部库

在我使用的过程中,我还遇到了如下ERROR: Unable to compile MEX function: “MEX 找不到使用 -l 选项指定的库 ‘ut’。

上网搜了一下后,我才知道MEX命令可以用-L选项指定第三方库的路径,用-l来连接第三方库文件。值得注意的是,这里使用的是该库文件的文件名,不包含其文件扩展名。其基本格式如下:

1
mex -L<library_path> -l<library>

可以发现,-L<library_path>-l<library>之间是没有加空格的。
可是看了这些,我还是解决不了我的问题。
这里说一下我出现这个问题的背景,最近接触计算机视觉中的目标跟踪这一块,正在学习vot-toolkit的使用。
我既问了度娘又问了谷哥,可是没有看到任何这个问题及其解决方法。于是我缩小范围,看了看Github上vot-toolkit的issues和VOT Challenge technical support的Google groups,惊喜的是的确都找到了同样的问题。


然而都只有问题没有解答。无奈,还是自己想办法吧。
其实跟着报错的提示来修改并不难,关键是要找到该修改哪里。
由于报错提示的函数中根本没有MEX连接库文件的指令(我一行一行代码找的),于是我想能不能找到MEX的编译文件。最终我在vot-toolkit-master\utilities中找到了一个名为compile_mex.m的matlab文件,其中有这样的一串代码。

1
2
3
4
5
if is_octave()
arguments{end+1} = '-DOCTAVE';
else
arguments{end+1} = '-lut';
end

我用的是matlab,不是octave(后者相当于轻量级的免费matlab,语法什么的基本一致),那么执行的应该是else后面的语句,而在这里可以看到调用ut的命令。
于是我就把这里的-lut改成了-Lut试了一下,果然成功了。
其实用有搜索功能的IDE的话或许能够更快地解决这个问题。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>之前在<a href="https://gsy00517.github.io/matlab20200115222641/" target="_b + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前在<a href="https://gsy00517.github.io/matlab20200115222641/" target="_b @@ -943,13 +943,13 @@ 2020-01-20T04:08:23.000Z 2020-03-27T15:19:19.784Z -

相关滤波(cross-correlation)是在目标跟踪领域一种非常强大的方法,主打简洁和高速,各种基于相关滤波的算法层出不穷。其中,KCF(不是肯德基)是一个非常经典的算法,在目标跟踪领域虽说它不是最早运用相关滤波的算法(MOSSE要早于它),但是它对之后运用相关滤波进行目标跟踪的这一系列算法有重要的奠基作用。本文就最近对这些方面的了解,结合自己的思考,做一个简单的整理归纳,如有疏漏之处还请多多指教。

References

电子文献:
https://blog.csdn.net/fhcfhc1112/article/details/83783588
https://blog.csdn.net/weixin_39467358/article/details/83304082
https://www.cnblogs.com/jins-note/p/10215511.html
https://blog.csdn.net/li_dongxuan/article/details/70667137?locationNum=5&fps=1
https://www.leiphone.com/news/201709/kLil97MnXF8Gh3sC.html

参考文献:
[1]High-Speed Tracking with Kernelized Correlation Filters
[2]Visual Object Tracking using Adaptive Correlation Filters
[3]Exploiting the Circulant Structure of Tracking-by-detection with Kernels


建议

由于KCF这篇文章主要是从理论上面来论述相关滤波来做tracking,其中涉及数学、理论的东西还是挺繁琐的。因此在正文开始之前,推荐可以先看一下我之前总结的几篇有关的博文,后文涉及到的话就不详细写了。
循环矩阵:linear-algebra笔记:循环矩阵
HOG特征:computer-vision笔记:HOG特征
正负样本:machine-learning笔记:数据采样之正样本和负样本
闭式解:machine-learning笔记:闭式解
此外,还可以先看看b站up主桥本环关于相关滤波两个算法的讲解视频目标跟踪:相关滤波算法MOSSE实现代码讲解目标跟踪:相关滤波算法KCF实现代码讲解,个人觉得讲得挺不错的。


论文

这里分别是MOSSEKCF两个算法的论文,我在阅读时已用黄色高亮一部分重点。同时本篇文章也主要参考了这两篇paper,可以先看,也可以看完本文再看。


相关滤波

先来看一个公式:

这里$:=$表示“定义为”(等效于等号上加个delta或者def),$\ast$表示复数共轭(complex-conjugate),而这里的$\star$表示的就是相关滤波操作。
你可能会觉得这个式子非常熟悉,是的,它非常像卷积的式子。但是不同的是,一般的卷积操作为$\int_{-\infty }^{\infty }f\left ( t \right )g\left ( t-\tau \right )dt$,而在相关滤波这里是加号。这就是说,在卷积的时候,我们需要把模板先进行翻转,再进行卷积,而相关操作就不需要了。
其实相关操作就是用来衡量两个信号是否相关,当两个信号越相似、相关性越强的时候,他们做相关操作输出的响应就会越强。用到目标跟踪里面,当做相关操作的两个框中的目标越相似,我们就会获得越高的响应。
相关滤波的实际意义是把输入图像映射到一个理想响应图,将这个响应图当中的最高峰与目标中心点对应起来,也就是我们预测的目标接下来的位置。它一个最主要的优点就是能够借助于傅里叶变换,从而快速计算大量的候选样本的响应值。


循环矩阵

在论文High-Speed Tracking with Kernelized Correlation Filters的introduction部分,有这样一句话:“we argue that undersampling negatives is the main factor inhibiting performance in tracking.”也就是说,负样本的欠采样是阻碍跟踪效果的主要因素。这在之前的文章中介绍过,这里就不在细述了。
这句话主要针对的问题是我们可以从一张图像中获得几乎无限的负样本。但由于跟踪的时间敏感性,我们的跟踪算法只能在尽可能多地采集样本和维持较低的计算需求之间取得一个平衡。之前通常的做法是从每一帧中随机选择几个样本。
KCF的一大贡献就是采用了一种更方便的方法迅速获取更多的负样本,以便于能够训练出一个更好的分类器。作者发现,在傅里叶域中,如果我们使用特定的模型进行转换,一些学习算法实际上变得更容易(in the Fourier domain, some learning algorithms actually become easier as we add more samples, if we use a specific model for translations)。
具体的做法如下,首先利用一个n维列向量来表示目标,记为$x$,然后利用$x$和一个循环移位矩阵$P$生成一个循环矩阵,其目的是使用一个base sample(正样本)和生成多个虚拟样本(负样本)来训练一个分类器。


根据循环特性可以推出下面两点:

  1. 可以周期性的获得同样的信号。
  2. 同样的,我们可以把上面的变换等效成将base sample即生成向量正向移动一半长度和反向移动一半长度组合而成。

这里是一个循环图片的示例,使用base sample,若我们向下移动15个像素,也就是从下面剪切15个像素拼到上面,就会变成左二图,若移动30个就可以生成左一图,右侧的图片是上移生成的。这就是在做tracking时循环采样的样本,一般会在目标周围取一个比目标更大的一个框,然后对大框框取的图像区域进行循环采样,那么就会生成这样一些新的样本来模拟我们的正样本并用于训练。

注意:这些新样本称为虚拟样本,不属于正样本但可以用于回归训练。

获得了这样一个循环矩阵之后,作者接下来说:“all circulant matrices are made diagonal by the Discrete Fourier Transform(DFT), regardless of the generating vector x.”就是说循环矩阵的生成向量是完全指定的,且循环矩阵有一个非常好的性质:对任意生成向量$\widehat{x}$,我们都可以通过离散傅立叶变换(具有线性性)对循环矩阵进行对角化表示。

这里的$F$是一个与向量$\widehat{x}$无关的常数矩阵,如果这里看得不懂的话,可以参照linear-algebra笔记:循环矩阵
如此,在傅里叶域内,用离散傅里叶变换来做之后的计算,对速度会有非常大的提升。
用到循环矩阵后,有两个常用的公式,可以参考我之前的文章。


训练

KCF做训练时所用的是岭回归。在线性情况下,岭回归的优化目标方程如下所示:

其闭式解为:

此时就可以利用循环矩阵在傅里叶域计算的性质来求解了。最后求出如下式子:

从原来的矩阵相乘和求逆,转换到傅里叶域的点乘和点除(这里的除号是点除),一下子运算就简单了许多。
在非线形的情况下,也可以得到一个同样的情况,这里需要引入一个满足条件的核,例如高斯核、线性核等,最后可计算得出一个闭式解。

非线性情况下,引入核可得到一个类似的岭回归的优化目标方程:

这里我们定义核函数$\kappa$为基向量$\varphi \left ( x \right )$之间的点积,即$\varphi^{T} \left ( x \right )\varphi \left ( x{}’ \right )=\kappa \left ( x,x{}’ \right )$。
在岭回归/脊回归(Ridge Regression)中,闭式解的基本形式如下:

这里$K$表示核空间的核矩阵,由核函数得到$K_{ij}=\kappa \left ( x_{i},x_{j} \right )=\varphi \left ( X \right )\varphi \left ( X \right )^{T}$。
最终可得:

这里的除号也是点除,此时求出来的$K$和$\alpha$就可以来做tracking了。同样的,我们还是利用循环矩阵的性质并且在傅里叶域内来做计算。


快速检测

我们很少希望单独来评估一个图像块的回归函数$f\left ( z \right )$。为了检测感兴趣的目标对象,我们通常希望在几个图像位置上评估$f\left ( z \right )$,这几个候选块(candidate patches)可以通过循环移位来建模。
定义$K^{z}$表示所有训练样本和所有候选块之间的核矩阵$K^{z}=\varphi \left ( X \right )\varphi \left ( Z \right )^{T}$。由于样本和图像块都是分别通过基础样本$x$和基础图像块$z$的循环移位组成的,因此矩阵$K^{z}$的每个元素可以表示为:$K_{i,j}=k(P^{i-1}z,P^{j-1}x)$。这里的$P$表示的是位移矩阵。易验证,$K^{z}$也是循环矩阵。
可计算得到各测试样本的响应值:

最后,我们可以求得一张feature map,也就是一张二维的响应图。


附录

这里补充在原论文的appendix中提到的两个问题。

  1. 余弦窗

    如果不加余弦窗,除了那个最原始样本,其他循环移位生成的样本的边缘都比较突兀,也就说这些样本数据是比较差的,会干扰训练的结果。而如果加了余弦窗。 上面是没加余弦窗的情况。除了那个最原始样本,其他样本都是合成的,如果不加余弦窗,那么这些合成样本就会降低分类器的判别能力(如上面c图所示,16张样本中只有两张比较合理)。如果加了余弦窗,就使得图像边缘像素值就都接近于或者等于0,因此循环移位过程中只要目标保持完整那这个样本就是合理的,只有当目标中心接近边缘时,目标跨越边界的那些样本才是错误的,这样以来虽不真实但合理的样本数量就可以增加到大约2/3。
  2. regression target y

    这个y是高斯加权后的值。初始目标的位置在padding后的search window的中心,循环移位得到的多个样本反应的是背景信息,而且离中心越远,就越不是目标,所以我们对标签进行高斯加权就刚好可以体现这种可能性准则。KCF里的输出是一个二维response矩阵,里面元素的大小代表该位置下的目标为预测目标的可能性,因此,在训练的时候就是输入是特征,而输出是一个gaussian_shaped_label,一般分类的标签是二值的,或者多值离散的,但是这个高斯标签反应的是由初始目标移位采样形成的若干样本距离初识样本越近可能性越大的准则,在代码中,高斯的峰值被移动到了左上角(于是四个角的值偏大),原因在论文的附录中进行了解释:“after computing a cross-correlation between two images in the Fourier domain and converting back to the spatial domain, it is the top-left element of the result that corresponds to a shift of zero”,也就是说目标零位移对应的是左上角的值。这样一来,我们在预测目标位置的时候,只需要pos=pos+find(response==max(response(:)))就好。如果把峰值放在中心点的话,就会“unnecessarily cause the detection output to be shifted by half a window”。

    补充:在代码中对目标进行padding是为了能让样本中含有特别需要学习的背景信息,而且可以尽量保证样本中目标的完整性,这是考虑循环移位将目标打散了。另外有关高斯加权,可以看一下我的文章computer-vision笔记:图像金字塔与高斯滤波器


效果

这是KCF在OTB2013上面做的一个实验,由于当时效果比较好的是struck(所以逃不了被针对的命运)。可以看到,KCF(使用HOG特征+高斯核函数)和DCF(也是同一个作者同一篇论文提出的,使用HOG特征+线性核函数,称为对偶相关滤波器)相比于struck来说,精度取得了显著的提升,从0.656提升到了0.732/0.728。

我们还可以根据这个统计图来看一下速度,即使使用了HOG特征和高斯核,KCF的速度还能达到172帧每秒。
此外,用了多通道扩展的DCF取得了更快的速度,但就精度而言较KCF稍差,但也是质的飞跃了。
相较而言,即使用了非常朴素的raw pixels,尽管效果比HOG特征差好多,但是速度并没有提高。这里也证明了HOG特征的强大。
另外可以看到KCF的祖宗MOSSE速度非常亮眼,但这是因为MOSSE它只用了简单的灰度特征,而不是HOG这样高维的特征,可想而知精确度总体效果还是要差一大截的。


缺点

  1. 缺点一

    对尺度变化的适应性不强。解决办法是加一个尺度变化的比例系数进行多次检测,代价是牺牲一些速度。
  2. 缺点二

    对目标快速变形(假设用的是HOG特征)或颜色快速变化(假设用的是颜色特征)不鲁棒。毕竟相关滤波是一种模板类的方法。HOG描述的就是形状信息,形状变化得太快必然会导致效果变差。而如果快速变色,那基于颜色特征的模板肯定也就跟不上了。这个还和模型更新策略与更新速度有关。若采用固定学习率的线性加权更新,那么如果学习率太大,部分或短暂遮挡和任何检测不准确,模型就会学习到背景信息,积累到一定程度模型被背景带飞了;如果学习率太小,目标已经变形了而模板还是那个模板,就会不认识目标,也会降低效果。
  3. 缺点三

    对物体快速运动或者低帧率视频不太鲁棒。这两种情况都是意味着在跟踪过程中下一帧图像中目标的位置偏离search window中心太远(要么靠近边缘,要么出去一半,要么全出去)。由于我们是给样本加了余弦窗的,也就是说目标位置靠近边缘会由于余弦窗的存在损失了部分目标信息(变成0),更不用说那些目标超出search window一半或者全超出去的情况了,这也就是CF类算法中的边界效应(Boundary Effects)。

其他

头一回看这么“理论”的论文我真的头都大了,要全部搞懂的话估计要花整整一天还不够。真的不得不佩服科研工作者们的智慧,我还是老老实实打基础吧。

原文的理论性、数学性更强,本文把主要的几个核心公式整理了一下,有些许修改和添加,如有疏漏还请多多指教。

]]>
+

相关滤波(cross-correlation)是在目标跟踪领域一种非常强大的方法,主打简洁和高速,各种基于相关滤波的算法层出不穷。其中,KCF(不是肯德基)是一个非常经典的算法,在目标跟踪领域虽说它不是最早运用相关滤波的算法(MOSSE要早于它),但是它对之后运用相关滤波进行目标跟踪的这一系列算法有重要的奠基作用。本文就最近对这些方面的了解,结合自己的思考,做一个简单的整理归纳,如有疏漏之处还请多多指教。

References

电子文献:
https://blog.csdn.net/fhcfhc1112/article/details/83783588
https://blog.csdn.net/weixin_39467358/article/details/83304082
https://www.cnblogs.com/jins-note/p/10215511.html
https://blog.csdn.net/li_dongxuan/article/details/70667137?locationNum=5&fps=1
https://www.leiphone.com/news/201709/kLil97MnXF8Gh3sC.html

参考文献:
[1]High-Speed Tracking with Kernelized Correlation Filters
[2]Visual Object Tracking using Adaptive Correlation Filters
[3]Exploiting the Circulant Structure of Tracking-by-detection with Kernels


建议

由于KCF这篇文章主要是从理论上面来论述相关滤波来做tracking,其中涉及数学、理论的东西还是挺繁琐的。因此在正文开始之前,推荐可以先看一下我之前总结的几篇有关的博文,后文涉及到的话就不详细写了。
循环矩阵:linear-algebra笔记:循环矩阵
HOG特征:computer-vision笔记:HOG特征
正负样本:machine-learning笔记:数据采样之正样本和负样本
闭式解:machine-learning笔记:闭式解
此外,还可以先看看b站up主桥本环关于相关滤波两个算法的讲解视频目标跟踪:相关滤波算法MOSSE实现代码讲解目标跟踪:相关滤波算法KCF实现代码讲解,个人觉得讲得挺不错的。


论文

这里分别是MOSSEKCF两个算法的论文,我在阅读时已用黄色高亮一部分重点。同时本篇文章也主要参考了这两篇paper,可以先看,也可以看完本文再看。


相关滤波

先来看一个公式:

这里$:=$表示“定义为”(等效于等号上加个delta或者def),$\ast$表示复数共轭(complex-conjugate),而这里的$\star$表示的就是相关滤波操作。
你可能会觉得这个式子非常熟悉,是的,它非常像卷积的式子。但是不同的是,一般的卷积操作为$\int_{-\infty }^{\infty }f\left ( t \right )g\left ( t-\tau \right )dt$,而在相关滤波这里是加号。这就是说,在卷积的时候,我们需要把模板先进行翻转,再进行卷积,而相关操作就不需要了。
其实相关操作就是用来衡量两个信号是否相关,当两个信号越相似、相关性越强的时候,他们做相关操作输出的响应就会越强。用到目标跟踪里面,当做相关操作的两个框中的目标越相似,我们就会获得越高的响应。
相关滤波的实际意义是把输入图像映射到一个理想响应图,将这个响应图当中的最高峰与目标中心点对应起来,也就是我们预测的目标接下来的位置。它一个最主要的优点就是能够借助于傅里叶变换,从而快速计算大量的候选样本的响应值。


循环矩阵

在论文High-Speed Tracking with Kernelized Correlation Filters的introduction部分,有这样一句话:“we argue that undersampling negatives is the main factor inhibiting performance in tracking.”也就是说,负样本的欠采样是阻碍跟踪效果的主要因素。这在之前的文章中介绍过,这里就不在细述了。
这句话主要针对的问题是我们可以从一张图像中获得几乎无限的负样本。但由于跟踪的时间敏感性,我们的跟踪算法只能在尽可能多地采集样本和维持较低的计算需求之间取得一个平衡。之前通常的做法是从每一帧中随机选择几个样本。
KCF的一大贡献就是采用了一种更方便的方法迅速获取更多的负样本,以便于能够训练出一个更好的分类器。作者发现,在傅里叶域中,如果我们使用特定的模型进行转换,一些学习算法实际上变得更容易(in the Fourier domain, some learning algorithms actually become easier as we add more samples, if we use a specific model for translations)。
具体的做法如下,首先利用一个n维列向量来表示目标,记为$x$,然后利用$x$和一个循环移位矩阵$P$生成一个循环矩阵,其目的是使用一个base sample(正样本)和生成多个虚拟样本(负样本)来训练一个分类器。


根据循环特性可以推出下面两点:

  1. 可以周期性的获得同样的信号。
  2. 同样的,我们可以把上面的变换等效成将base sample即生成向量正向移动一半长度和反向移动一半长度组合而成。

这里是一个循环图片的示例,使用base sample,若我们向下移动15个像素,也就是从下面剪切15个像素拼到上面,就会变成左二图,若移动30个就可以生成左一图,右侧的图片是上移生成的。这就是在做tracking时循环采样的样本,一般会在目标周围取一个比目标更大的一个框,然后对大框框取的图像区域进行循环采样,那么就会生成这样一些新的样本来模拟我们的正样本并用于训练。

注意:这些新样本称为虚拟样本,不属于正样本但可以用于回归训练。

获得了这样一个循环矩阵之后,作者接下来说:“all circulant matrices are made diagonal by the Discrete Fourier Transform(DFT), regardless of the generating vector x.”就是说循环矩阵的生成向量是完全指定的,且循环矩阵有一个非常好的性质:对任意生成向量$\widehat{x}$,我们都可以通过离散傅立叶变换(具有线性性)对循环矩阵进行对角化表示。

这里的$F$是一个与向量$\widehat{x}$无关的常数矩阵,如果这里看得不懂的话,可以参照linear-algebra笔记:循环矩阵
如此,在傅里叶域内,用离散傅里叶变换来做之后的计算,对速度会有非常大的提升。
用到循环矩阵后,有两个常用的公式,可以参考我之前的文章。


训练

KCF做训练时所用的是岭回归。在线性情况下,岭回归的优化目标方程如下所示:

其闭式解为:

此时就可以利用循环矩阵在傅里叶域计算的性质来求解了。最后求出如下式子:

从原来的矩阵相乘和求逆,转换到傅里叶域的点乘和点除(这里的除号是点除),一下子运算就简单了许多。
在非线形的情况下,也可以得到一个同样的情况,这里需要引入一个满足条件的核,例如高斯核、线性核等,最后可计算得出一个闭式解。

非线性情况下,引入核可得到一个类似的岭回归的优化目标方程:

这里我们定义核函数$\kappa$为基向量$\varphi \left ( x \right )$之间的点积,即$\varphi^{T} \left ( x \right )\varphi \left ( x{}’ \right )=\kappa \left ( x,x{}’ \right )$。
在岭回归/脊回归(Ridge Regression)中,闭式解的基本形式如下:

这里$K$表示核空间的核矩阵,由核函数得到$K_{ij}=\kappa \left ( x_{i},x_{j} \right )=\varphi \left ( X \right )\varphi \left ( X \right )^{T}$。
最终可得:

这里的除号也是点除,此时求出来的$K$和$\alpha$就可以来做tracking了。同样的,我们还是利用循环矩阵的性质并且在傅里叶域内来做计算。


快速检测

我们很少希望单独来评估一个图像块的回归函数$f\left ( z \right )$。为了检测感兴趣的目标对象,我们通常希望在几个图像位置上评估$f\left ( z \right )$,这几个候选块(candidate patches)可以通过循环移位来建模。
定义$K^{z}$表示所有训练样本和所有候选块之间的核矩阵$K^{z}=\varphi \left ( X \right )\varphi \left ( Z \right )^{T}$。由于样本和图像块都是分别通过基础样本$x$和基础图像块$z$的循环移位组成的,因此矩阵$K^{z}$的每个元素可以表示为:$K_{i,j}=k(P^{i-1}z,P^{j-1}x)$。这里的$P$表示的是位移矩阵。易验证,$K^{z}$也是循环矩阵。
可计算得到各测试样本的响应值:

最后,我们可以求得一张feature map,也就是一张二维的响应图。


附录

这里补充在原论文的appendix中提到的两个问题。

  1. 余弦窗

    如果不加余弦窗,除了那个最原始样本,其他循环移位生成的样本的边缘都比较突兀,也就说这些样本数据是比较差的,会干扰训练的结果。而如果加了余弦窗。 上面是没加余弦窗的情况。除了那个最原始样本,其他样本都是合成的,如果不加余弦窗,那么这些合成样本就会降低分类器的判别能力(如上面c图所示,16张样本中只有两张比较合理)。如果加了余弦窗,就使得图像边缘像素值就都接近于或者等于0,因此循环移位过程中只要目标保持完整那这个样本就是合理的,只有当目标中心接近边缘时,目标跨越边界的那些样本才是错误的,这样以来虽不真实但合理的样本数量就可以增加到大约2/3。
  2. regression target y

    这个y是高斯加权后的值。初始目标的位置在padding后的search window的中心,循环移位得到的多个样本反应的是背景信息,而且离中心越远,就越不是目标,所以我们对标签进行高斯加权就刚好可以体现这种可能性准则。KCF里的输出是一个二维response矩阵,里面元素的大小代表该位置下的目标为预测目标的可能性,因此,在训练的时候就是输入是特征,而输出是一个gaussian_shaped_label,一般分类的标签是二值的,或者多值离散的,但是这个高斯标签反应的是由初始目标移位采样形成的若干样本距离初识样本越近可能性越大的准则,在代码中,高斯的峰值被移动到了左上角(于是四个角的值偏大),原因在论文的附录中进行了解释:“after computing a cross-correlation between two images in the Fourier domain and converting back to the spatial domain, it is the top-left element of the result that corresponds to a shift of zero”,也就是说目标零位移对应的是左上角的值。这样一来,我们在预测目标位置的时候,只需要pos=pos+find(response==max(response(:)))就好。如果把峰值放在中心点的话,就会“unnecessarily cause the detection output to be shifted by half a window”。

    补充:在代码中对目标进行padding是为了能让样本中含有特别需要学习的背景信息,而且可以尽量保证样本中目标的完整性,这是考虑循环移位将目标打散了。另外有关高斯加权,可以看一下我的文章computer-vision笔记:图像金字塔与高斯滤波器


效果

这是KCF在OTB2013上面做的一个实验,由于当时效果比较好的是struck(所以逃不了被针对的命运)。可以看到,KCF(使用HOG特征+高斯核函数)和DCF(也是同一个作者同一篇论文提出的,使用HOG特征+线性核函数,称为对偶相关滤波器)相比于struck来说,精度取得了显著的提升,从0.656提升到了0.732/0.728。

我们还可以根据这个统计图来看一下速度,即使使用了HOG特征和高斯核,KCF的速度还能达到172帧每秒。
此外,用了多通道扩展的DCF取得了更快的速度,但就精度而言较KCF稍差,但也是质的飞跃了。
相较而言,即使用了非常朴素的raw pixels,尽管效果比HOG特征差好多,但是速度并没有提高。这里也证明了HOG特征的强大。
另外可以看到KCF的祖宗MOSSE速度非常亮眼,但这是因为MOSSE它只用了简单的灰度特征,而不是HOG这样高维的特征,可想而知精确度总体效果还是要差一大截的。


缺点

  1. 缺点一

    对尺度变化的适应性不强。解决办法是加一个尺度变化的比例系数进行多次检测,代价是牺牲一些速度。
  2. 缺点二

    对目标快速变形(假设用的是HOG特征)或颜色快速变化(假设用的是颜色特征)不鲁棒。毕竟相关滤波是一种模板类的方法。HOG描述的就是形状信息,形状变化得太快必然会导致效果变差。而如果快速变色,那基于颜色特征的模板肯定也就跟不上了。这个还和模型更新策略与更新速度有关。若采用固定学习率的线性加权更新,那么如果学习率太大,部分或短暂遮挡和任何检测不准确,模型就会学习到背景信息,积累到一定程度模型被背景带飞了;如果学习率太小,目标已经变形了而模板还是那个模板,就会不认识目标,也会降低效果。
  3. 缺点三

    对物体快速运动或者低帧率视频不太鲁棒。这两种情况都是意味着在跟踪过程中下一帧图像中目标的位置偏离search window中心太远(要么靠近边缘,要么出去一半,要么全出去)。由于我们是给样本加了余弦窗的,也就是说目标位置靠近边缘会由于余弦窗的存在损失了部分目标信息(变成0),更不用说那些目标超出search window一半或者全超出去的情况了,这也就是CF类算法中的边界效应(Boundary Effects)。

其他

头一回看这么“理论”的论文我真的头都大了,要全部搞懂的话估计要花整整一天还不够。真的不得不佩服科研工作者们的智慧,我还是老老实实打基础吧。

原文的理论性、数学性更强,本文把主要的几个核心公式整理了一下,有些许修改和添加,如有疏漏还请多多指教。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>相关滤波(cross-correlation)是在目标跟踪领域一种非常强大的方法,主打简洁和高速,各种基于相关滤波的算法层出不穷。其中,KCF( + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>相关滤波(cross-correlation)是在目标跟踪领域一种非常强大的方法,主打简洁和高速,各种基于相关滤波的算法层出不穷。其中,KCF( @@ -973,13 +973,13 @@ 2020-01-19T15:10:33.000Z 2020-01-28T01:47:30.677Z -

computer-vision笔记:HOG特征一文中,我曾提及了Image Pyramid。那么,这个图像金字塔究竟是一个什么名胜古迹呢?

References

电子文献:
https://www.cnblogs.com/ronny/p/3886013.html
https://www.cnblogs.com/wynlfd/p/9704770.html
https://www.cnblogs.com/herenzhiming/articles/5276106.html
https://www.jianshu.com/p/73e6ccbd8f3f
https://baike.baidu.com/item/%E9%AB%98%E6%96%AF%E6%BB%A4%E6%B3%A2/9032353?fr=aladdin
https://blog.csdn.net/lvquanye9483/article/details/81592574
https://zhuanlan.zhihu.com/p/94014493


原因

当用一个机器视觉系统分析未知场景时,计算机没有办法预先知识图像中物体尺度,因此,我们需要同时考虑图像在多尺度下的描述,获知感兴趣物体的最佳尺度。在很多时候,我们会将图像构建为一系列不同尺度的图像集,在不同的尺度中去检测我们感兴趣的特征。比如:在Haar特征检测人脸的时候,因为我们并不知道图像中人脸的尺寸,所以需要生成一个不同大小的图像组成的“金字塔”,扫描其中每一幅图像来寻找可能的人脸。


图像金字塔

我们可以这样得到一个图像金字塔:首先,图像经过一个低通滤波器进行平滑处理(这个步骤会使图像变模糊,类似远处的物体没有近处的清晰),然后,对这个平滑处理后的图像进行抽样(一般抽样比例在水平和竖直方向上都为1/2),从而得到一系列缩小的图像。

假设高斯金字塔的第$l$层图像为$G_{l}$,则有:

其中,$N$为高斯金字塔的层数,$R_{l}$和$C_{l}$分别为高斯金字塔第$l$层的行数和列数,$\omega \left ( m,n \right )$是一个二位可拆的5x5窗口函数,其表达式为:

上式说明,2维窗口的卷积算子,可以写成两个方向上的1维卷积核的乘积。上面卷积形式的公式实际上完成了两个功能:(1)高斯模糊;(2)降维。
按上述步骤生成的$G_{0}$,$G_{1}$,…,$G_{N}$就构成了图像的高斯金字塔,其中$G_{N}$为金字塔的底层(与原图像相同),$G_{N}$为金字塔的顶层。可见高斯金字塔的当前层图像是对其前一层图像进行高斯低通滤波、然后做隔行和隔列的降采样(去除偶数行与偶数列)生成的。其中每一层都是前一层图像大小的1/4。


高斯滤波器

本文讨论图像金字塔,怎么说着说着就变成高斯金字塔了呢。事实上,上面所提到的$\omega$其实是一个整数值的高斯核函数,进行了高斯滤波的平滑处理。

高斯滤波

这里先引入两个问题:

  1. 为什么要对图像滤波?
    主要有两个目的:(1)消除图像在数字化过程中产生或者混入的噪声;(2)提取图片对象的特征作为图像识别的特征模式。
  2. 如何理解滤波器?
    这与电路中的滤波器类似但又不同,在图像处理中,滤波器可以想象成一个包含加权系数的窗口,当使用滤波器去处理图像时,输出就相当于通过这个窗口去看这个图像。

滤波的方式有很多种,而高斯滤波是一种线性平滑滤波,适用于消除高斯噪声。
在图像处理中,高斯滤波一般有两种实现方式,一是用离散化窗口滑窗卷积,另一种通过傅里叶变换。最常见的就是第一种滑窗实现,只有当离散化的窗口非常大,用滑窗计算量非常大(尽管用了可分离滤波器依旧大)的情况下,可能会考虑基于傅里叶变化的实现方法。本文介绍的是滑窗卷积法。

高斯噪声

首先,噪声在图像中常表现为一引起较强视觉效果的孤立像素点或像素块。
而高斯噪声,就是噪声的概率密度函数服从正态分布。

高斯函数

我们首先回顾一下概率论中的高斯函数。
一维高斯分布:$G\left ( x \right )=\frac{1}{\sqrt{2\pi }\sigma }e^{-\frac{x^{2}}{2\sigma ^{2}}}$。

二维高斯分布:$G\left ( x,y \right )=\frac{1}{\sqrt{2\pi }\sigma ^{2}}e^{-\frac{x^{2}+y^{2}}{2\sigma ^{2}}}$

注:$\sigma$越大,高斯函数的高度越小,宽度越大。

如上图所示,正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。
我们只需要将“中心点”作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。这也是高斯核函数的构造原理。
理论上,高斯分布在所有定义域上都有非负值,这就需要一个无限大的卷积核。实际上,仅需要取均值周围3倍标准差内的值($3\sigma$准则),以外的部分可以直接去掉。

这里取$\sigma=1.5$,注意,由于要对这9个点计算加权平均,因此必须让它们的权重之和等于1,上图是最终所得的高斯模板。

高斯模糊

模糊处理的过程其实就是卷积的过程,使用上面的高斯模板,对图像的每一个像素进行卷积,就能使图像产生模糊平滑的效果。

上图最左边是原图9个像素点的灰度值,最右边是输出的结果。要注意的是,一次这样的操作实际上只得到了中心点的输出,即所求的加权平均结果为中心点的高斯滤波输出值。

可分离滤波器

如上面图像金字塔一节所述,2维窗口的卷积算子,可以写成两个方向上的1维卷积核的乘积。由于高斯函数可以写成可分离的形式,因此可以采用可分离滤波器实现来加速。所谓的可分离滤波器,就是可以把多维的卷积化成多个一维卷积。具体到二维的高斯滤波,就是指先对行做一维卷积,再对列做一维卷积。这样就可以将计算时间复杂度从$O\left ( M\ast M\ast N\ast N \right )$降到$O\left ( 2\ast M\ast M\ast N \right )$,这里的$M$和$N$分别是图像和滤波器的窗口大小。

性质

  1. 旋转对称性

    二维高斯函数具有旋转对称性,即滤波器在各个方向上的平滑程度是相同的。一般一幅图像的边缘方向事先是不知道的,因此,在滤波前是无法确定一个方向上比另一方向上需要更多的平滑。而旋转对称性意味着高斯平滑滤波器在后续边缘检测中不会偏向任一方向。
  2. 平滑程度易调

    高斯滤波器宽度(决定着平滑程度)是由参数$\sigma$表征的,而且$\sigma$和平滑程度的关系是非常简单的。$\sigma$越大,高斯滤波器的频带就越宽,平滑程度就越好。通过调节平滑程度参数$\sigma$,可在图像特征过分模糊(过平滑)与平滑图像中由于噪声和细纹理所引起的过多的不希望突变量(欠平滑)之间取得balance。
    此外,高斯函数是单值函数。这表明,高斯滤波器用像素邻域的加权均值来代替该点的像素值,而每一邻域像素点权值是随该点与中心点的距离单调增减的。这一性质很重要,因为边缘是一种图像局部特征,如果平滑运算对离算子中心很远的像素点仍然有很大作用,则平滑运算会使图像失真。
  3. 平滑可以层叠

    由性质:两个高斯核的卷积等同于另外一个不同核参数的高斯核卷积。可以推得:不同的高斯核对图像的平滑是连续的。
  4. 可分离性

    根据上文分析,由于高斯函数的可分离性,大高斯滤波器可以高效地实现。二维高斯函数卷积可以分两步来进行,首先将图像与一维高斯核进行卷积,然后将卷积结果与方向垂直且函数形式相同一维高斯核再进行卷积。如此,二维高斯滤波的计算量随滤波模板宽度$N$成线性增长而不是成平方增长。

值得一提的是,在Young对生理学的研究中发现,哺乳动物的视网膜和视觉皮层的感受区域可以很好地用4阶以内的高斯微分来建模。


拉普拉斯金字塔

由于高斯金字塔用于图片下采样(即减小图片的尺寸),是从金字塔的底层到上层自下而上的。而高斯滤波构造的图像金字塔具有局部极值递性,即图像的特征是在减少的。
那么我们自然而然会想到,需不需要一个自上而下的金字塔用于上采样,和高斯金字塔配合使用。
这里就引入了拉普拉斯金字塔,它可以认为是一个残差金字塔,用来存储下采样后图片与原始图片的差异。其每一层的图像为同一层高斯金字塔的图像减去上一层的图像进行上采样并高斯模糊的结果。

注意,高斯金字塔的下采样是不可逆的,可以这样理解:下采样过程丢失的信息不能通过上采样来完全恢复,即高斯金字塔中任意一张图$G_{i}$先进行下采样得到图$Down(G_{i})$,再进行上采样得到图$Up(Down(G_{i}))$,此时的$Up(Down(G_{i}))$与原本的$G_{i}$是存在差异的。而拉普拉斯金字塔的作用,就是记录高斯金字塔每一层下采样后再上采样得到的结果与下采样前的原图之间差异,其目的是为了能够完整的恢复出每一层的下采样前图像。可以用下面这个公式来简单表述:

若将第$i+1$层的高斯金字塔从顶层开始依次加上第$i$层拉普拉斯金子塔,那么就几乎可以复原原来图像(有点绕,建议结合上图体会)。
拉普拉斯的具体构造过程如下:

  1. 内插

    将$G_{l}$进行内插(这里是用与降维时相同的滤波核而不是双线性插值),得到放大的图像$G_{l}^{\ast }$,使$G_{l}^{\ast }$的尺寸与$G_{l-1}$的尺寸相同,表示为:这边的参数就不说明了,与上文相同。需要注意的是,这里的系数取$4$,是因为每次能参与加权的项的权值之和为4/256,这与$\omega$的选取有关。
  2. 相减

    原理上文已经说了,接下来我们自上而下构造拉普拉斯金字塔。

如下图所示,此为文章前面小猫的图像金字塔所生成的拉普拉斯金字塔。不要以为这张图搞错了,细看的话就会发现,除了顶层的图片之外,下面的图片(大尺寸的)中仅有一些零散的点(不是屏幕上的灰尘)和淡淡的线,也就是残差。可以通过Gamma校正使这些残差特征更加清晰。

关于Gamma校正,可以看一看上一篇文章computer-vision笔记:HOG特征,关于图像金字塔和高斯滤波器就说到这里了。

]]>
+

computer-vision笔记:HOG特征一文中,我曾提及了Image Pyramid。那么,这个图像金字塔究竟是一个什么名胜古迹呢?

References

电子文献:
https://www.cnblogs.com/ronny/p/3886013.html
https://www.cnblogs.com/wynlfd/p/9704770.html
https://www.cnblogs.com/herenzhiming/articles/5276106.html
https://www.jianshu.com/p/73e6ccbd8f3f
https://baike.baidu.com/item/%E9%AB%98%E6%96%AF%E6%BB%A4%E6%B3%A2/9032353?fr=aladdin
https://blog.csdn.net/lvquanye9483/article/details/81592574
https://zhuanlan.zhihu.com/p/94014493


原因

当用一个机器视觉系统分析未知场景时,计算机没有办法预先知识图像中物体尺度,因此,我们需要同时考虑图像在多尺度下的描述,获知感兴趣物体的最佳尺度。在很多时候,我们会将图像构建为一系列不同尺度的图像集,在不同的尺度中去检测我们感兴趣的特征。比如:在Haar特征检测人脸的时候,因为我们并不知道图像中人脸的尺寸,所以需要生成一个不同大小的图像组成的“金字塔”,扫描其中每一幅图像来寻找可能的人脸。


图像金字塔

我们可以这样得到一个图像金字塔:首先,图像经过一个低通滤波器进行平滑处理(这个步骤会使图像变模糊,类似远处的物体没有近处的清晰),然后,对这个平滑处理后的图像进行抽样(一般抽样比例在水平和竖直方向上都为1/2),从而得到一系列缩小的图像。

假设高斯金字塔的第$l$层图像为$G_{l}$,则有:

其中,$N$为高斯金字塔的层数,$R_{l}$和$C_{l}$分别为高斯金字塔第$l$层的行数和列数,$\omega \left ( m,n \right )$是一个二位可拆的5x5窗口函数,其表达式为:

上式说明,2维窗口的卷积算子,可以写成两个方向上的1维卷积核的乘积。上面卷积形式的公式实际上完成了两个功能:(1)高斯模糊;(2)降维。
按上述步骤生成的$G_{0}$,$G_{1}$,…,$G_{N}$就构成了图像的高斯金字塔,其中$G_{N}$为金字塔的底层(与原图像相同),$G_{N}$为金字塔的顶层。可见高斯金字塔的当前层图像是对其前一层图像进行高斯低通滤波、然后做隔行和隔列的降采样(去除偶数行与偶数列)生成的。其中每一层都是前一层图像大小的1/4。


高斯滤波器

本文讨论图像金字塔,怎么说着说着就变成高斯金字塔了呢。事实上,上面所提到的$\omega$其实是一个整数值的高斯核函数,进行了高斯滤波的平滑处理。

高斯滤波

这里先引入两个问题:

  1. 为什么要对图像滤波?
    主要有两个目的:(1)消除图像在数字化过程中产生或者混入的噪声;(2)提取图片对象的特征作为图像识别的特征模式。
  2. 如何理解滤波器?
    这与电路中的滤波器类似但又不同,在图像处理中,滤波器可以想象成一个包含加权系数的窗口,当使用滤波器去处理图像时,输出就相当于通过这个窗口去看这个图像。

滤波的方式有很多种,而高斯滤波是一种线性平滑滤波,适用于消除高斯噪声。
在图像处理中,高斯滤波一般有两种实现方式,一是用离散化窗口滑窗卷积,另一种通过傅里叶变换。最常见的就是第一种滑窗实现,只有当离散化的窗口非常大,用滑窗计算量非常大(尽管用了可分离滤波器依旧大)的情况下,可能会考虑基于傅里叶变化的实现方法。本文介绍的是滑窗卷积法。

高斯噪声

首先,噪声在图像中常表现为一引起较强视觉效果的孤立像素点或像素块。
而高斯噪声,就是噪声的概率密度函数服从正态分布。

高斯函数

我们首先回顾一下概率论中的高斯函数。
一维高斯分布:$G\left ( x \right )=\frac{1}{\sqrt{2\pi }\sigma }e^{-\frac{x^{2}}{2\sigma ^{2}}}$。

二维高斯分布:$G\left ( x,y \right )=\frac{1}{\sqrt{2\pi }\sigma ^{2}}e^{-\frac{x^{2}+y^{2}}{2\sigma ^{2}}}$

注:$\sigma$越大,高斯函数的高度越小,宽度越大。

如上图所示,正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。
我们只需要将“中心点”作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。这也是高斯核函数的构造原理。
理论上,高斯分布在所有定义域上都有非负值,这就需要一个无限大的卷积核。实际上,仅需要取均值周围3倍标准差内的值($3\sigma$准则),以外的部分可以直接去掉。

这里取$\sigma=1.5$,注意,由于要对这9个点计算加权平均,因此必须让它们的权重之和等于1,上图是最终所得的高斯模板。

高斯模糊

模糊处理的过程其实就是卷积的过程,使用上面的高斯模板,对图像的每一个像素进行卷积,就能使图像产生模糊平滑的效果。

上图最左边是原图9个像素点的灰度值,最右边是输出的结果。要注意的是,一次这样的操作实际上只得到了中心点的输出,即所求的加权平均结果为中心点的高斯滤波输出值。

可分离滤波器

如上面图像金字塔一节所述,2维窗口的卷积算子,可以写成两个方向上的1维卷积核的乘积。由于高斯函数可以写成可分离的形式,因此可以采用可分离滤波器实现来加速。所谓的可分离滤波器,就是可以把多维的卷积化成多个一维卷积。具体到二维的高斯滤波,就是指先对行做一维卷积,再对列做一维卷积。这样就可以将计算时间复杂度从$O\left ( M\ast M\ast N\ast N \right )$降到$O\left ( 2\ast M\ast M\ast N \right )$,这里的$M$和$N$分别是图像和滤波器的窗口大小。

性质

  1. 旋转对称性

    二维高斯函数具有旋转对称性,即滤波器在各个方向上的平滑程度是相同的。一般一幅图像的边缘方向事先是不知道的,因此,在滤波前是无法确定一个方向上比另一方向上需要更多的平滑。而旋转对称性意味着高斯平滑滤波器在后续边缘检测中不会偏向任一方向。
  2. 平滑程度易调

    高斯滤波器宽度(决定着平滑程度)是由参数$\sigma$表征的,而且$\sigma$和平滑程度的关系是非常简单的。$\sigma$越大,高斯滤波器的频带就越宽,平滑程度就越好。通过调节平滑程度参数$\sigma$,可在图像特征过分模糊(过平滑)与平滑图像中由于噪声和细纹理所引起的过多的不希望突变量(欠平滑)之间取得balance。
    此外,高斯函数是单值函数。这表明,高斯滤波器用像素邻域的加权均值来代替该点的像素值,而每一邻域像素点权值是随该点与中心点的距离单调增减的。这一性质很重要,因为边缘是一种图像局部特征,如果平滑运算对离算子中心很远的像素点仍然有很大作用,则平滑运算会使图像失真。
  3. 平滑可以层叠

    由性质:两个高斯核的卷积等同于另外一个不同核参数的高斯核卷积。可以推得:不同的高斯核对图像的平滑是连续的。
  4. 可分离性

    根据上文分析,由于高斯函数的可分离性,大高斯滤波器可以高效地实现。二维高斯函数卷积可以分两步来进行,首先将图像与一维高斯核进行卷积,然后将卷积结果与方向垂直且函数形式相同一维高斯核再进行卷积。如此,二维高斯滤波的计算量随滤波模板宽度$N$成线性增长而不是成平方增长。

值得一提的是,在Young对生理学的研究中发现,哺乳动物的视网膜和视觉皮层的感受区域可以很好地用4阶以内的高斯微分来建模。


拉普拉斯金字塔

由于高斯金字塔用于图片下采样(即减小图片的尺寸),是从金字塔的底层到上层自下而上的。而高斯滤波构造的图像金字塔具有局部极值递性,即图像的特征是在减少的。
那么我们自然而然会想到,需不需要一个自上而下的金字塔用于上采样,和高斯金字塔配合使用。
这里就引入了拉普拉斯金字塔,它可以认为是一个残差金字塔,用来存储下采样后图片与原始图片的差异。其每一层的图像为同一层高斯金字塔的图像减去上一层的图像进行上采样并高斯模糊的结果。

注意,高斯金字塔的下采样是不可逆的,可以这样理解:下采样过程丢失的信息不能通过上采样来完全恢复,即高斯金字塔中任意一张图$G_{i}$先进行下采样得到图$Down(G_{i})$,再进行上采样得到图$Up(Down(G_{i}))$,此时的$Up(Down(G_{i}))$与原本的$G_{i}$是存在差异的。而拉普拉斯金字塔的作用,就是记录高斯金字塔每一层下采样后再上采样得到的结果与下采样前的原图之间差异,其目的是为了能够完整的恢复出每一层的下采样前图像。可以用下面这个公式来简单表述:

若将第$i+1$层的高斯金字塔从顶层开始依次加上第$i$层拉普拉斯金子塔,那么就几乎可以复原原来图像(有点绕,建议结合上图体会)。
拉普拉斯的具体构造过程如下:

  1. 内插

    将$G_{l}$进行内插(这里是用与降维时相同的滤波核而不是双线性插值),得到放大的图像$G_{l}^{\ast }$,使$G_{l}^{\ast }$的尺寸与$G_{l-1}$的尺寸相同,表示为:这边的参数就不说明了,与上文相同。需要注意的是,这里的系数取$4$,是因为每次能参与加权的项的权值之和为4/256,这与$\omega$的选取有关。
  2. 相减

    原理上文已经说了,接下来我们自上而下构造拉普拉斯金字塔。

如下图所示,此为文章前面小猫的图像金字塔所生成的拉普拉斯金字塔。不要以为这张图搞错了,细看的话就会发现,除了顶层的图片之外,下面的图片(大尺寸的)中仅有一些零散的点(不是屏幕上的灰尘)和淡淡的线,也就是残差。可以通过Gamma校正使这些残差特征更加清晰。

关于Gamma校正,可以看一看上一篇文章computer-vision笔记:HOG特征,关于图像金字塔和高斯滤波器就说到这里了。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>在<a href="https://gsy00517.github.io/computer-vision20200119195116/" tar + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在<a href="https://gsy00517.github.io/computer-vision20200119195116/" tar @@ -999,13 +999,13 @@ 2020-01-19T11:51:16.000Z 2020-03-15T04:19:52.070Z -

这几天感觉知识的海洋真的是无边无际的,一旦查资料就意味着要查更多的资料,不懂的东西简直一个接着一个。希望某天能对这些知识有个大概的把握吧,废话不多说,开始积累!

References

电子文献:
https://www.cnblogs.com/hrlnw/archive/2013/08/06/2826651.html
https://www.cnblogs.com/zyly/p/9651261.html
https://baike.baidu.com/item/HOG/9738560?fr=aladdin
https://zhuanlan.zhihu.com/p/40960756
https://www.jianshu.com/p/354acdcbae3f
https://blog.csdn.net/xjp_xujiping/article/details/89430002
https://www.leiphone.com/news/201708/ZKsGd2JRKr766wEd.html

参考文献:
[1]Histograms of Oriented Gradients for Human Detection
[2]Understanding and Diagnosing Visual Tracking Systems


特征描述子

特征描述子就是图像的一种表示,这种表示抽取了有用的信息,丢掉了不相关的信息。通常特征描述子会把一个WxHx3(3个channel)的图像转换成一个向量或者一个矩阵。


HOG特征

HOG(Histogram of Oriented Gradient),中文译为方向梯度直方图。它是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子,通过计算像局部区域中不同方向上梯度的值,然后进行累积,得到代表这块区域特征的直方图,可将其输入到分类器里面进行目标检测。
下面是论文中所呈现的进行人物检测时所采用的特征提取与目标检测的流程,本文将主要分析特征提取的部分(论文中对运用SVM分类器做检测的部分分析甚少,因此不做重点)。


思想

HOG特征的主要思想是:在图像中,局部目标的表象和形状能够被梯度或边缘的方向密度分布很好地描述。由于,梯度主要存在于边缘的地方,因此其密度分布能一定程度上描述目标物体的形状等特征。


思路

在介绍HOG特征提取的基本思路之前,我先放一张图,我认为先了解block和cell有助于理解思路,否则有点抽象,可能会(像我当初一样)看得很迷。下图的分割尺度与后文的分析一致,即一个block分成2x2的cell,一个cell分成8x8的pixel,可以先记下来。

HOG特征提取的基本思路是:

  1. 为了减少光照因素的影响,首先需要将整个图像进行归一化,这种压缩处理能够有效地降低图像局部的阴影和光照变化。
  2. 将图像分成很多小的cell,采集cell中各像素点梯度的幅值和方向,然后在每个cell中统计出一个一维的梯度方向直方图。
  3. 为了对光照和阴影有更好的不变性,我们可以在更大的范围内,对block进行对比度归一化。
  4. 在归一化后,最终获得的即为HOG描述子。

这里共做了两次normalization,不要搞混了。
那么,为什么要分成很多小的cell呢?因为用特征描述子的一个主要原因是它提供了一种紧凑的压缩表示。后面我们会看到如何把一个9个bin的直方图表示成9个数的数组。此外,对每个cell统计梯度直方图不仅可以使表示压缩而紧凑,用直方图来表示一个patch还可以使其可以更加抗噪,因为一个gradient可能会有噪音,但是用直方图来表示后就不会对噪音那么敏感了。


流程

  1. Normalize gamma & colour

    由于在图像的纹理强度中,局部的表层曝光贡献的比重比较大,因此为了减少光照因素的影响,我们首先采用Gamma校正法对输入图像的颜色空间进行归一化。
    Gamma校正可以提高图像中偏暗或者偏亮部分的对比效果,能有效降低图像局部的阴影和光照变化。其校正公式为:

    其中$I$为图像像素值,$\gamma$为校正系数。
    由幂函数的性质容易推得Gamma校正的作用,这里以灰度图像(即仅有黑白,单通道)为例做一个简单解释。

    当$\gamma$小于1时,在低灰度值区域内,动态范围变大,图像对比度增强;在高灰度值区域,动态范围变小,图像对比度降低。图像的整体灰度值变大,如下面中间的图片(左边是原图)。
    当$\gamma$大于1时,在低灰度值区域内,动态范围变小,图像对比度降低;在高灰度值区域,动态范围变大,图像对比度提高。图像的整体灰度值变小,如下面右边的图片。

    注:灰度值也称灰度等级,范围一般从0到255,白色为255,黑色为0。

    然而,是否使用Gamma校正还要视具体情况而定,当涉及大量的类内颜色变化时,比如斑马等自身就颜色变化丰富的物体,不校正效果会更好。
    上面的例子使用的是灰度图像,事实上,RGB彩色图(三通道)的performance会更好一些。可以看一下下面的对比图。

  2. Image segmentation

    分割图像这一步在论文的流程图中没有,我觉得有必要说一下,因此自行添加了一步。
    这里先说一下为什么要进行图像分割。如果对一大幅图片直接提取特征,往往得不到好的效果。因为如果提取区域比较大,那么两个完全不同的图像,也可能提取出相似的HOG特征。但这种可能性在较小的区域就很小。此外,当我们把图像分割成很多区块(patch)然后对每个区块提取特征时,这其中也包含了几何上的位置特性。例如,正面的人脸,左上部分的图像区块提取的HOG特征一般是和眼睛的HOG特征相符合的。
    HOG的图像分割策略一般有overlap和non-overlap两种,如下图所示。第一张图是overlap的情况,指的是分割出的区块会有互相重合的区域。而non-overlap指的是区块不交叠,没有重合区域。 overlap的优点在于这种分割方式可以防止对一些物体的切割,例如,如果分割的时候正好把一个眼睛从中间切割分到了两个patch中,那么提取完HOG特征之后,可能会影响接下来的分类效果。但是如果两个patch之间有一个overlap,那么这里的三个patch至少有一个内会保留完整的眼睛。overlap的缺点在于计算量大,因为重叠区域的像素需要重复计算。
    non-overlap恰恰相反,其缺点如上所述,就是有时会将一个连续的物体切割开,得到不太好的HOG特征。但它的优点是计算量小,尤其是与图像金字塔(Image Pyramid,详见computer-vision笔记:图像金字塔与高斯滤波器)相结合时,这个优点更为明显。
    在这里,我们用overlap的策略将图像分割成一个个互相重叠的block。也就是说,每个cell的直方图都会被多次用于最终的特征描述子的计算。虽然这看起来有冗余,但可以显著地提升性能。
  3. Compute gradients

    接下来就是计算图像每个像素点梯度。$G_{y}\left ( x,y \right )$算法同理。
    在具体实现时,我们可以使用如下两种kernel来实现上面梯度的计算。 接着计算梯度的幅值和方向。 可以看到,图像的梯度去掉了很多不必要的信息(比如不变的背景色),并且加重了轮廓。
  4. Weighted vote into spatial & orientation cells

    将图像划分成小的cell(矩形或者环形),然后统计每一个cell的梯度直方图,即可以得到一个cell的描述符。
    注意,这里我们在一个block内划分4个cell,即2×2个cell组成一个block,8x8个像素点组成一个cell。将一个block内每个cell的描述符串联起来即可得到一个block的HOG描述符。
    我们先来看一下各个像素点的梯度的具体计算情况。 统计梯度直方图的方式是利用刚才计算得出的cell中每一个像素点的梯度进行加权投票。我们一般考虑采用9个bin的直方图来统计一个cell中像素的梯度信息,即将cell的梯度方向0~180°(无向)或0~360°(考虑正负)分成9个bin,如下图所示: 如果cell中某一像素的梯度方向是20~40°,那么上面这个直方图第2个bin的计数就要加1,这样对cell中的每一个像素用梯度方向在直方图中进行加权投影(权值大小等于梯度幅值),将其映射到对应角度范围的bin内,就可以得到这个cell的梯度方向直方图了,即该cell对应的一个9维特征向量。若梯度方向位于相邻bin的交界处(如20°、40°等),需要对其进行方向和位置上的双线性插值。 上图是一个比较直观的加权投票的演示,这里将投影在交界处的梯度的幅值对半分至两侧的bin。
    要注意的是,如果一个角度大于160度,也就是在160至180度之间,我们知道这里角度0和180度是一样的,所以在下面这个例子里,像素的角度为165度时,就要把幅值按照比例放到0和160的bin里面去,也就是上面说的双线性插值。 事实上,我们可以采用幅值本身或者它的函数(幅值的平方根、幅值的平方、幅值的截断形式)来表示权值,但经实际测试表明:使用幅值来表示权值能获得最佳的效果。采用梯度幅值作为权重,可以使那些比较明显的边缘的方向信息对特征表达影响增大,这样比较合理,因为HOG特征主要就是依靠这些边缘纹理。
    经实验还发现,采用无向的梯度(即0~180°)和9个bin的直方图,能在行人检测试验中取得最佳的效果。
  5. Contrast normalize over overlapping spatial blocks

    由于局部光照的变化,以及前景背景对比度的变化,使得梯度强度的变化范围非常大,这就需要对梯度做局部对比度归一化。归一化能够进一步对光照、阴影、边缘进行压缩,使得特征向量对光照、阴影和边缘变化具有鲁棒性。
    我们之前已将2x2的cell组成了更大的block,现在要做的就是针对每个block进行对比度归一化。
    通常使用的HOG结构大致有三种:矩形HOG(R-HOG),圆形HOG(C-HOG)和中心环绕HOG。它们的单位都是block。实验证明,矩形HOG和圆形HOG的检测效果基本一致,而环绕形HOG效果相对差一些。 我们一般根据如下公式对block进行对比度归一化:这里$v$是该block未经归一化的特征向量,比如这里一个block含2x2个cell,每个cell对应一个9维的特征向量,那么这个block的特征向量的长度就是2x2x9。这里$\xi$是一个很小的数,主要是为了防止分母等于零而引入的。
  6. Collect HOG’s over detection window

    我们先review一下,上面我们首先把样本图片分割为若干个像素的cell,然后把梯度方向划分为9个区间,在每个cell里面对所有像素的梯度方向在各个区间进行直方图统计,得到一个9维的特征向量;然后我们使每相邻4个cell构成一个block,把一个block内的特征向量串联起来得到一个36(即2x2x9)维的特征向量。
    接下来,我们用block对样本图像进行扫描,stride为一个cell的大小,扫描完成后,我们将扫描过程中每个block的特征向量(即上述36维的)串联起来,得到样本的特征向量,也就是可以输入到后面分类器的HOG特征描述子。
    如此,就完成了HOG特征的提取。
    我们可以可视化一下最后的HOG特征,可见主要捕捉的是博尔特的躯干和腿。

总结

优点

  1. HOG特性能较好地捕捉局部形状信息,对几何和光学变化都有很好的不变性。
  2. HOG特征是在密集采样的图像块中求取的,在计算得到的HOG特征向量中隐含了该块与检测窗口之间的几何空间位置关系。

不足

  1. 很难处理遮挡问题,人体姿势动作幅度过大或物体方向改变时不易检测(这个问题后来在DPM中采用可变形部件模型的方法得到了改善)。
  2. 没有选取主方向,也没有旋转梯度方向直方图,因而本身不具有旋转不变性(较大的方向变化),其旋转不变性是通过采用不同旋转方向的训练样本来实现的。
  3. 本身不具有尺度不变性,其尺度不变性是通过缩放检测窗口图像的大小(如Image Pyramid)来实现的。
  4. 由于梯度的性质,HOG对噪点相当敏感,在实际应用中,在block和cell划分之后,对于得到各个区域,有时候还会做一次高斯平滑去除噪点。但又由于HOG特征是基于边缘的,平滑操作会降低边缘信息的对比度,从而减少图像中的有用信息,因此还需好好做个balance。

FHOG

由于后续的研究发现HOG特征可以降维。因此在HOG的基础上,又提出了FHOG(我理解为fast HOG),也取得了广泛的应用。


计算机视觉女神

或许会发现本文中好多的图例都用了同一位女士的脸,这里就扯点题外话。
照片中的女子名为Lena Soderberg,对计算机视觉领域有一定的接触的朋友应该对这张照片不会陌生。这张照片是标准的数字图像处理用例,各种算法研究经常会使用这张图作为模板。那为什么要用这幅图呢?
David C. Munson在“A Note on Lena”中给出了两条理由:

  1. 首先,该图像中各个频段的能量都很丰富,既有低频(光滑的皮肤),也有高频(帽子上的羽毛),适度地混合了细节、平滑区域、阴影和纹理,很适合来测试各种图像处理算法。
  2. 其次,Lena是位迷人的女子,能有效吸引研究者(大部分为男性)做研究。

该照片其实是一张于1972年11月出版的Playboy的中间插页。1973年,南加州大学信号图像处理研究所的副教授Alexander和学生一起,为了一个同事的学会论文正忙于寻找一幅好的图片。他们想要一幅具有良好动态范围的人的面部图片用于扫描。这时,不知是谁拿着一本Playboy走进研究室。由于当时实验室里使用的扫描仪(Muirhead wirephoto scanner)分辨率是100行每英寸,试验也仅仅需要一副512X512的图片,所以他们只将图片顶端开始的5.12英寸扫描下来,并切掉肩膀以下的部分。多年以来,由于图像Lena源于Playboy,将其引用于科技文章中饱受争议。Playboy杂志也将未授权的引用告上法庭。但随着时间的流逝,人们渐渐淡忘Lena的来源,Playboy也放松了对此的关注。值得一提的是,Lena也是Playboy发行的最畅销的海报,已经出售7,161,561份。其真正刊登的原图如下。

1997年,在图像科学和技术协会的第50届会议上,Lenna被邀为贵宾出席。在会议上,她忙于签名、拍照以及介绍自我。

说实话每次看到这张照片,想起学术界那些大牛们居然能弄出这样一个挺有意思的故事,不免觉得也挺有趣的。在我最喜欢的美剧之一《Silicon Valley》中,也有计算机视觉女神的身影。

重温大结局的时候发现也出现了。

大家对计算机视觉有兴趣的话,其实自己也打印一张摆墙上,这样就可以和懂的小伙伴们一起讨论问题了。

]]>
+

这几天感觉知识的海洋真的是无边无际的,一旦查资料就意味着要查更多的资料,不懂的东西简直一个接着一个。希望某天能对这些知识有个大概的把握吧,废话不多说,开始积累!

References

电子文献:
https://www.cnblogs.com/hrlnw/archive/2013/08/06/2826651.html
https://www.cnblogs.com/zyly/p/9651261.html
https://baike.baidu.com/item/HOG/9738560?fr=aladdin
https://zhuanlan.zhihu.com/p/40960756
https://www.jianshu.com/p/354acdcbae3f
https://blog.csdn.net/xjp_xujiping/article/details/89430002
https://www.leiphone.com/news/201708/ZKsGd2JRKr766wEd.html

参考文献:
[1]Histograms of Oriented Gradients for Human Detection
[2]Understanding and Diagnosing Visual Tracking Systems


特征描述子

特征描述子就是图像的一种表示,这种表示抽取了有用的信息,丢掉了不相关的信息。通常特征描述子会把一个WxHx3(3个channel)的图像转换成一个向量或者一个矩阵。


HOG特征

HOG(Histogram of Oriented Gradient),中文译为方向梯度直方图。它是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子,通过计算像局部区域中不同方向上梯度的值,然后进行累积,得到代表这块区域特征的直方图,可将其输入到分类器里面进行目标检测。
下面是论文中所呈现的进行人物检测时所采用的特征提取与目标检测的流程,本文将主要分析特征提取的部分(论文中对运用SVM分类器做检测的部分分析甚少,因此不做重点)。


思想

HOG特征的主要思想是:在图像中,局部目标的表象和形状能够被梯度或边缘的方向密度分布很好地描述。由于,梯度主要存在于边缘的地方,因此其密度分布能一定程度上描述目标物体的形状等特征。


思路

在介绍HOG特征提取的基本思路之前,我先放一张图,我认为先了解block和cell有助于理解思路,否则有点抽象,可能会(像我当初一样)看得很迷。下图的分割尺度与后文的分析一致,即一个block分成2x2的cell,一个cell分成8x8的pixel,可以先记下来。

HOG特征提取的基本思路是:

  1. 为了减少光照因素的影响,首先需要将整个图像进行归一化,这种压缩处理能够有效地降低图像局部的阴影和光照变化。
  2. 将图像分成很多小的cell,采集cell中各像素点梯度的幅值和方向,然后在每个cell中统计出一个一维的梯度方向直方图。
  3. 为了对光照和阴影有更好的不变性,我们可以在更大的范围内,对block进行对比度归一化。
  4. 在归一化后,最终获得的即为HOG描述子。

这里共做了两次normalization,不要搞混了。
那么,为什么要分成很多小的cell呢?因为用特征描述子的一个主要原因是它提供了一种紧凑的压缩表示。后面我们会看到如何把一个9个bin的直方图表示成9个数的数组。此外,对每个cell统计梯度直方图不仅可以使表示压缩而紧凑,用直方图来表示一个patch还可以使其可以更加抗噪,因为一个gradient可能会有噪音,但是用直方图来表示后就不会对噪音那么敏感了。


流程

  1. Normalize gamma & colour

    由于在图像的纹理强度中,局部的表层曝光贡献的比重比较大,因此为了减少光照因素的影响,我们首先采用Gamma校正法对输入图像的颜色空间进行归一化。
    Gamma校正可以提高图像中偏暗或者偏亮部分的对比效果,能有效降低图像局部的阴影和光照变化。其校正公式为:

    其中$I$为图像像素值,$\gamma$为校正系数。
    由幂函数的性质容易推得Gamma校正的作用,这里以灰度图像(即仅有黑白,单通道)为例做一个简单解释。

    当$\gamma$小于1时,在低灰度值区域内,动态范围变大,图像对比度增强;在高灰度值区域,动态范围变小,图像对比度降低。图像的整体灰度值变大,如下面中间的图片(左边是原图)。
    当$\gamma$大于1时,在低灰度值区域内,动态范围变小,图像对比度降低;在高灰度值区域,动态范围变大,图像对比度提高。图像的整体灰度值变小,如下面右边的图片。

    注:灰度值也称灰度等级,范围一般从0到255,白色为255,黑色为0。

    然而,是否使用Gamma校正还要视具体情况而定,当涉及大量的类内颜色变化时,比如斑马等自身就颜色变化丰富的物体,不校正效果会更好。
    上面的例子使用的是灰度图像,事实上,RGB彩色图(三通道)的performance会更好一些。可以看一下下面的对比图。

  2. Image segmentation

    分割图像这一步在论文的流程图中没有,我觉得有必要说一下,因此自行添加了一步。
    这里先说一下为什么要进行图像分割。如果对一大幅图片直接提取特征,往往得不到好的效果。因为如果提取区域比较大,那么两个完全不同的图像,也可能提取出相似的HOG特征。但这种可能性在较小的区域就很小。此外,当我们把图像分割成很多区块(patch)然后对每个区块提取特征时,这其中也包含了几何上的位置特性。例如,正面的人脸,左上部分的图像区块提取的HOG特征一般是和眼睛的HOG特征相符合的。
    HOG的图像分割策略一般有overlap和non-overlap两种,如下图所示。第一张图是overlap的情况,指的是分割出的区块会有互相重合的区域。而non-overlap指的是区块不交叠,没有重合区域。 overlap的优点在于这种分割方式可以防止对一些物体的切割,例如,如果分割的时候正好把一个眼睛从中间切割分到了两个patch中,那么提取完HOG特征之后,可能会影响接下来的分类效果。但是如果两个patch之间有一个overlap,那么这里的三个patch至少有一个内会保留完整的眼睛。overlap的缺点在于计算量大,因为重叠区域的像素需要重复计算。
    non-overlap恰恰相反,其缺点如上所述,就是有时会将一个连续的物体切割开,得到不太好的HOG特征。但它的优点是计算量小,尤其是与图像金字塔(Image Pyramid,详见computer-vision笔记:图像金字塔与高斯滤波器)相结合时,这个优点更为明显。
    在这里,我们用overlap的策略将图像分割成一个个互相重叠的block。也就是说,每个cell的直方图都会被多次用于最终的特征描述子的计算。虽然这看起来有冗余,但可以显著地提升性能。
  3. Compute gradients

    接下来就是计算图像每个像素点梯度。$G_{y}\left ( x,y \right )$算法同理。
    在具体实现时,我们可以使用如下两种kernel来实现上面梯度的计算。 接着计算梯度的幅值和方向。 可以看到,图像的梯度去掉了很多不必要的信息(比如不变的背景色),并且加重了轮廓。
  4. Weighted vote into spatial & orientation cells

    将图像划分成小的cell(矩形或者环形),然后统计每一个cell的梯度直方图,即可以得到一个cell的描述符。
    注意,这里我们在一个block内划分4个cell,即2×2个cell组成一个block,8x8个像素点组成一个cell。将一个block内每个cell的描述符串联起来即可得到一个block的HOG描述符。
    我们先来看一下各个像素点的梯度的具体计算情况。 统计梯度直方图的方式是利用刚才计算得出的cell中每一个像素点的梯度进行加权投票。我们一般考虑采用9个bin的直方图来统计一个cell中像素的梯度信息,即将cell的梯度方向0~180°(无向)或0~360°(考虑正负)分成9个bin,如下图所示: 如果cell中某一像素的梯度方向是20~40°,那么上面这个直方图第2个bin的计数就要加1,这样对cell中的每一个像素用梯度方向在直方图中进行加权投影(权值大小等于梯度幅值),将其映射到对应角度范围的bin内,就可以得到这个cell的梯度方向直方图了,即该cell对应的一个9维特征向量。若梯度方向位于相邻bin的交界处(如20°、40°等),需要对其进行方向和位置上的双线性插值。 上图是一个比较直观的加权投票的演示,这里将投影在交界处的梯度的幅值对半分至两侧的bin。
    要注意的是,如果一个角度大于160度,也就是在160至180度之间,我们知道这里角度0和180度是一样的,所以在下面这个例子里,像素的角度为165度时,就要把幅值按照比例放到0和160的bin里面去,也就是上面说的双线性插值。 事实上,我们可以采用幅值本身或者它的函数(幅值的平方根、幅值的平方、幅值的截断形式)来表示权值,但经实际测试表明:使用幅值来表示权值能获得最佳的效果。采用梯度幅值作为权重,可以使那些比较明显的边缘的方向信息对特征表达影响增大,这样比较合理,因为HOG特征主要就是依靠这些边缘纹理。
    经实验还发现,采用无向的梯度(即0~180°)和9个bin的直方图,能在行人检测试验中取得最佳的效果。
  5. Contrast normalize over overlapping spatial blocks

    由于局部光照的变化,以及前景背景对比度的变化,使得梯度强度的变化范围非常大,这就需要对梯度做局部对比度归一化。归一化能够进一步对光照、阴影、边缘进行压缩,使得特征向量对光照、阴影和边缘变化具有鲁棒性。
    我们之前已将2x2的cell组成了更大的block,现在要做的就是针对每个block进行对比度归一化。
    通常使用的HOG结构大致有三种:矩形HOG(R-HOG),圆形HOG(C-HOG)和中心环绕HOG。它们的单位都是block。实验证明,矩形HOG和圆形HOG的检测效果基本一致,而环绕形HOG效果相对差一些。 我们一般根据如下公式对block进行对比度归一化:这里$v$是该block未经归一化的特征向量,比如这里一个block含2x2个cell,每个cell对应一个9维的特征向量,那么这个block的特征向量的长度就是2x2x9。这里$\xi$是一个很小的数,主要是为了防止分母等于零而引入的。
  6. Collect HOG’s over detection window

    我们先review一下,上面我们首先把样本图片分割为若干个像素的cell,然后把梯度方向划分为9个区间,在每个cell里面对所有像素的梯度方向在各个区间进行直方图统计,得到一个9维的特征向量;然后我们使每相邻4个cell构成一个block,把一个block内的特征向量串联起来得到一个36(即2x2x9)维的特征向量。
    接下来,我们用block对样本图像进行扫描,stride为一个cell的大小,扫描完成后,我们将扫描过程中每个block的特征向量(即上述36维的)串联起来,得到样本的特征向量,也就是可以输入到后面分类器的HOG特征描述子。
    如此,就完成了HOG特征的提取。
    我们可以可视化一下最后的HOG特征,可见主要捕捉的是博尔特的躯干和腿。

总结

优点

  1. HOG特性能较好地捕捉局部形状信息,对几何和光学变化都有很好的不变性。
  2. HOG特征是在密集采样的图像块中求取的,在计算得到的HOG特征向量中隐含了该块与检测窗口之间的几何空间位置关系。

不足

  1. 很难处理遮挡问题,人体姿势动作幅度过大或物体方向改变时不易检测(这个问题后来在DPM中采用可变形部件模型的方法得到了改善)。
  2. 没有选取主方向,也没有旋转梯度方向直方图,因而本身不具有旋转不变性(较大的方向变化),其旋转不变性是通过采用不同旋转方向的训练样本来实现的。
  3. 本身不具有尺度不变性,其尺度不变性是通过缩放检测窗口图像的大小(如Image Pyramid)来实现的。
  4. 由于梯度的性质,HOG对噪点相当敏感,在实际应用中,在block和cell划分之后,对于得到各个区域,有时候还会做一次高斯平滑去除噪点。但又由于HOG特征是基于边缘的,平滑操作会降低边缘信息的对比度,从而减少图像中的有用信息,因此还需好好做个balance。

FHOG

由于后续的研究发现HOG特征可以降维。因此在HOG的基础上,又提出了FHOG(我理解为fast HOG),也取得了广泛的应用。


计算机视觉女神

或许会发现本文中好多的图例都用了同一位女士的脸,这里就扯点题外话。
照片中的女子名为Lena Soderberg,对计算机视觉领域有一定的接触的朋友应该对这张照片不会陌生。这张照片是标准的数字图像处理用例,各种算法研究经常会使用这张图作为模板。那为什么要用这幅图呢?
David C. Munson在“A Note on Lena”中给出了两条理由:

  1. 首先,该图像中各个频段的能量都很丰富,既有低频(光滑的皮肤),也有高频(帽子上的羽毛),适度地混合了细节、平滑区域、阴影和纹理,很适合来测试各种图像处理算法。
  2. 其次,Lena是位迷人的女子,能有效吸引研究者(大部分为男性)做研究。

该照片其实是一张于1972年11月出版的Playboy的中间插页。1973年,南加州大学信号图像处理研究所的副教授Alexander和学生一起,为了一个同事的学会论文正忙于寻找一幅好的图片。他们想要一幅具有良好动态范围的人的面部图片用于扫描。这时,不知是谁拿着一本Playboy走进研究室。由于当时实验室里使用的扫描仪(Muirhead wirephoto scanner)分辨率是100行每英寸,试验也仅仅需要一副512X512的图片,所以他们只将图片顶端开始的5.12英寸扫描下来,并切掉肩膀以下的部分。多年以来,由于图像Lena源于Playboy,将其引用于科技文章中饱受争议。Playboy杂志也将未授权的引用告上法庭。但随着时间的流逝,人们渐渐淡忘Lena的来源,Playboy也放松了对此的关注。值得一提的是,Lena也是Playboy发行的最畅销的海报,已经出售7,161,561份。其真正刊登的原图如下。

1997年,在图像科学和技术协会的第50届会议上,Lenna被邀为贵宾出席。在会议上,她忙于签名、拍照以及介绍自我。

说实话每次看到这张照片,想起学术界那些大牛们居然能弄出这样一个挺有意思的故事,不免觉得也挺有趣的。在我最喜欢的美剧之一《Silicon Valley》中,也有计算机视觉女神的身影。

重温大结局的时候发现也出现了。

大家对计算机视觉有兴趣的话,其实自己也打印一张摆墙上,这样就可以和懂的小伙伴们一起讨论问题了。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>这几天感觉知识的海洋真的是无边无际的,一旦查资料就意味着要查更多的资料,不懂的东西简直一个接着一个。希望某天能对这些知识有个大概的把握吧,废话不 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>这几天感觉知识的海洋真的是无边无际的,一旦查资料就意味着要查更多的资料,不懂的东西简直一个接着一个。希望某天能对这些知识有个大概的把握吧,废话不 @@ -1025,13 +1025,13 @@ 2020-01-19T06:56:37.000Z 2020-02-19T02:06:47.510Z -

看论文总是会看到许多新奇的名词,比如“one-shot learning”(单样本学习)即仅从一个或者很少的样本中学习训练。还有诸如“closed-form”、“closed-form solution”,看的我真是很纳闷,这里就结合资料好好理理清楚。

References

电子文献:
https://blog.csdn.net/yy16808/article/details/76493384
https://blog.csdn.net/langjueyun2010/article/details/80348449


解析解与数值解

这里先介绍两个相对应的数学概念:解析解与数值解。
直接上例子:解$x^{2}=3$。
那么这题的解析解是:$x=\sqrt{3}$。
数值解为:$x=1.732$。
简而言之,解析解就是给出解的具体函数形式,从解的表达式中就可以算出任何对应值,好像就是小学所谓的公式解;而数值解就是直接用数值方法求出具体的解。


闭式解

实际上,闭式解也被称为解析解。由于解析解为一封闭形式(closed-form)的函数,因此对任一独立变量,我们皆可将其带入解析函数求得正确的相依变量。即解可以表达为一个函数形式,带入变量即可得到解。
一般而言,有闭式解的优化方法效率更高。
这就写完了,似乎很简单。

]]>
+

看论文总是会看到许多新奇的名词,比如“one-shot learning”(单样本学习)即仅从一个或者很少的样本中学习训练。还有诸如“closed-form”、“closed-form solution”,看的我真是很纳闷,这里就结合资料好好理理清楚。

References

电子文献:
https://blog.csdn.net/yy16808/article/details/76493384
https://blog.csdn.net/langjueyun2010/article/details/80348449


解析解与数值解

这里先介绍两个相对应的数学概念:解析解与数值解。
直接上例子:解$x^{2}=3$。
那么这题的解析解是:$x=\sqrt{3}$。
数值解为:$x=1.732$。
简而言之,解析解就是给出解的具体函数形式,从解的表达式中就可以算出任何对应值,好像就是小学所谓的公式解;而数值解就是直接用数值方法求出具体的解。


闭式解

实际上,闭式解也被称为解析解。由于解析解为一封闭形式(closed-form)的函数,因此对任一独立变量,我们皆可将其带入解析函数求得正确的相依变量。即解可以表达为一个函数形式,带入变量即可得到解。
一般而言,有闭式解的优化方法效率更高。
这就写完了,似乎很简单。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>看论文总是会看到许多新奇的名词,比如“one-shot learning”(单样本学习)即仅从一个或者很少的样本中学习训练。还有诸如“close + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>看论文总是会看到许多新奇的名词,比如“one-shot learning”(单样本学习)即仅从一个或者很少的样本中学习训练。还有诸如“close @@ -1051,13 +1051,13 @@ 2020-01-18T13:39:42.000Z 2020-02-23T13:15:20.682Z -

在Visual Object Tracking using Adaptive Correlation Filters一文中,我看到这样一句话:“The Peak-to-Sidelobe Ratio(PSR), which measures the strength of a correlation peak, can be used to detect occlusions or tracking failure, to stop the online update, and to reacquire the track if the object reappears with a similar appearance.”其大意为:用来衡量相关峰值强度的Peak-to-Sidelobe Ratio可以用于检测遮挡或者跟踪失误、停止实时更新和在相似外观再次出现时重新获取跟踪路径。我读完在想:这个PSR是什么?这么有用。结果百度了半天翻了几页没发现有任何有关它介绍或者解释的文章。于是看了一些英文文献,将自己的一些浅见写下来。

注:近期发现在DCF类的tracker中PSR得到了比较广泛的应用,所以原谅我当初论文看得少哈~

References

电子文献:
https://baike.baidu.com/item/pslr/19735601

参考文献:
[1]Understanding and Diagnosing Visual Tracking Systems
[2]Visual Object Tracking using Adaptive Correlation Filters
[3]Adaptive Model Update via Fusing Peak-to-sidelobe Ratio and Mean Frame Difference for Visual Tracking
[4]Multi-Cue Correlation Filters for Robust Visual Tracking


What

由于没有找到任何中文翻译,我只能取一个相近的(感觉指的差不多)。在百度百科中,我找到了如下解释:PSLR,峰值旁瓣比,peak side lobe ratio,定义为主瓣峰值强度对于最强旁瓣的峰值强度之比,多用于雷达信号脉冲压缩后对信号的评估。

注意:百度百科内的名词与我遇到的相差了一个“to”,且缩写也不同。

上面是百度百科内的一个配图,根据百科解释,最高的主瓣与第二高的主瓣之差,即是PLSR(峰值旁瓣比),在这里大小为-13.4dB。
此外,我还找到了一个称为峰均比(PAPR,peak-to-average power ratio)的概念,它等于波形的振幅和其有效值(RMS)之比,主要是针对功率的,这里就不细说了。
后来,我在一篇光学期刊的文章上找到了一个比较可靠的翻译,该文献中有一个名为“峰旁比”的名词,且文献内容与目标追踪相关。因此我暂且称其为峰旁比吧。个人觉得比峰值旁瓣比简洁且好听。


Why

其实中文翻译并不重要,重要的是它的作用。
在查阅文献的过程中,我看到了这样一个公式:

其中$P_{sr}^{t}$是此时第t帧的峰旁比,$f_{t}$是对于第t帧分类器预测的响应,$\mu _{t}$和$\sigma _{t}$分别是响应图$f$的均值和方差。
根据这种计算方法,我们可以大概分析一下峰旁比的作用。当初看到时,我觉得它与我在物理实验中做过的音叉共振实验中的品质因数有相似之处(品质因数$Q=\frac{f_{0}}{f_{2}-f_{1}}$)。
由公式可知,当响应中的峰值较高,且响应分布相对而言集中在峰值及周围时,峰旁比就会较高;反之,峰旁比就会较低。
那么什么时候会造成峰旁比较低呢?根据论文描述可以获得提示,当遇到遮挡,或者目标跟丢即响应区内不含目标主体时,就不会出现一个那么明显的峰值响应,同时响应也会较为分散了,此时分母较大、分子较小,峰旁比就会变低。
下面也是Visual Object Tracking using Adaptive Correlation Filters中的一张figure(这篇文章提出了MOSSE),我们需要的是a much stronger peak which translates into less drift and fewer dropped tracks。看了这个想必应该知道峰旁比发挥的作用和大致原因了。

和上面的峰值旁瓣比、峰均比相比较,显然峰旁比的定义能更好地表征响应的集中程度。


How

由峰旁比定义所得出的性质可知,峰旁比可以作为模型预测定位准确性和可信度的判据。我们可以利用峰旁比来调整Model Updater(The model updater controls the strategy and frequency of updating the observation model. It has to strike a balance between model adaptation and drift.)
我的想法是,我们可以设置一个threshold,当峰旁比小于这个threshold时,表示模型未能准确定位到目标(可信度较低),这时我们可以停止模型的更新,或者通过减小学习率等方法减慢模型的更新速度以防止模型受背景或者遮挡物较大影响,而当目标再次出现时难以复原导致最终完全跟丢的问题;而当峰旁比大于这个threshold时,我们可以实时更新模型,或者运用较大的学习率(也可以根据峰旁比将学习率划分成几个等级)。

]]>
+

在Visual Object Tracking using Adaptive Correlation Filters一文中,我看到这样一句话:“The Peak-to-Sidelobe Ratio(PSR), which measures the strength of a correlation peak, can be used to detect occlusions or tracking failure, to stop the online update, and to reacquire the track if the object reappears with a similar appearance.”其大意为:用来衡量相关峰值强度的Peak-to-Sidelobe Ratio可以用于检测遮挡或者跟踪失误、停止实时更新和在相似外观再次出现时重新获取跟踪路径。我读完在想:这个PSR是什么?这么有用。结果百度了半天翻了几页没发现有任何有关它介绍或者解释的文章。于是看了一些英文文献,将自己的一些浅见写下来。

注:近期发现在DCF类的tracker中PSR得到了比较广泛的应用,所以原谅我当初论文看得少哈~

References

电子文献:
https://baike.baidu.com/item/pslr/19735601

参考文献:
[1]Understanding and Diagnosing Visual Tracking Systems
[2]Visual Object Tracking using Adaptive Correlation Filters
[3]Adaptive Model Update via Fusing Peak-to-sidelobe Ratio and Mean Frame Difference for Visual Tracking
[4]Multi-Cue Correlation Filters for Robust Visual Tracking


What

由于没有找到任何中文翻译,我只能取一个相近的(感觉指的差不多)。在百度百科中,我找到了如下解释:PSLR,峰值旁瓣比,peak side lobe ratio,定义为主瓣峰值强度对于最强旁瓣的峰值强度之比,多用于雷达信号脉冲压缩后对信号的评估。

注意:百度百科内的名词与我遇到的相差了一个“to”,且缩写也不同。

上面是百度百科内的一个配图,根据百科解释,最高的主瓣与第二高的主瓣之差,即是PLSR(峰值旁瓣比),在这里大小为-13.4dB。
此外,我还找到了一个称为峰均比(PAPR,peak-to-average power ratio)的概念,它等于波形的振幅和其有效值(RMS)之比,主要是针对功率的,这里就不细说了。
后来,我在一篇光学期刊的文章上找到了一个比较可靠的翻译,该文献中有一个名为“峰旁比”的名词,且文献内容与目标追踪相关。因此我暂且称其为峰旁比吧。个人觉得比峰值旁瓣比简洁且好听。


Why

其实中文翻译并不重要,重要的是它的作用。
在查阅文献的过程中,我看到了这样一个公式:

其中$P_{sr}^{t}$是此时第t帧的峰旁比,$f_{t}$是对于第t帧分类器预测的响应,$\mu _{t}$和$\sigma _{t}$分别是响应图$f$的均值和方差。
根据这种计算方法,我们可以大概分析一下峰旁比的作用。当初看到时,我觉得它与我在物理实验中做过的音叉共振实验中的品质因数有相似之处(品质因数$Q=\frac{f_{0}}{f_{2}-f_{1}}$)。
由公式可知,当响应中的峰值较高,且响应分布相对而言集中在峰值及周围时,峰旁比就会较高;反之,峰旁比就会较低。
那么什么时候会造成峰旁比较低呢?根据论文描述可以获得提示,当遇到遮挡,或者目标跟丢即响应区内不含目标主体时,就不会出现一个那么明显的峰值响应,同时响应也会较为分散了,此时分母较大、分子较小,峰旁比就会变低。
下面也是Visual Object Tracking using Adaptive Correlation Filters中的一张figure(这篇文章提出了MOSSE),我们需要的是a much stronger peak which translates into less drift and fewer dropped tracks。看了这个想必应该知道峰旁比发挥的作用和大致原因了。

和上面的峰值旁瓣比、峰均比相比较,显然峰旁比的定义能更好地表征响应的集中程度。


How

由峰旁比定义所得出的性质可知,峰旁比可以作为模型预测定位准确性和可信度的判据。我们可以利用峰旁比来调整Model Updater(The model updater controls the strategy and frequency of updating the observation model. It has to strike a balance between model adaptation and drift.)
我的想法是,我们可以设置一个threshold,当峰旁比小于这个threshold时,表示模型未能准确定位到目标(可信度较低),这时我们可以停止模型的更新,或者通过减小学习率等方法减慢模型的更新速度以防止模型受背景或者遮挡物较大影响,而当目标再次出现时难以复原导致最终完全跟丢的问题;而当峰旁比大于这个threshold时,我们可以实时更新模型,或者运用较大的学习率(也可以根据峰旁比将学习率划分成几个等级)。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>在Visual Object Tracking using Adaptive Correlation Filters一文中,我看到这样一句话:“ + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在Visual Object Tracking using Adaptive Correlation Filters一文中,我看到这样一句话:“ @@ -1079,13 +1079,13 @@ 2020-01-18T03:21:56.000Z 2020-01-20T05:28:14.019Z -

今天在看最小二乘回归(least squares regression)时看到作者把positive examples设成1,把negative examples设成0。感觉对这个概念既熟悉又陌生,查了一下之后一下子想起来了。在机器学习中,数据预处理一般包括数据清洗、数据集成、数据采样。而正负样本涉及到了数据采样的问题,因此后面也提一下。


正样本和负样本

简单来说,和概率论中类似,一般我们看一个问题时,只关注一个事件(希望它发生或者成功,并对其进行分析计算),而正样本就是属于我们关注的这一类别的样本,负样本就是指不属于该类别的样本。


数据采样平衡

一般来说,比如我们训练分类器时,希望样本中正负样本的比例是接近于1:1的。因为如果正样本占比很大(比如90%)或者负样本占比远超正样本,那么训练结果可想而知,获得的分类器在测试中的效果会很差。
针对这种数据不平衡的问题,有以下三种solution:

  1. 过采样(over-sampling)

    这是一种较为直接的办法,即通过随机复制少数类来增加其中的实例数量,从而可增加样本中少数类的代表性。
  2. 欠采样(under-sampling)

    这种方法也比较直接,即通过随机消除占多数类的样本来平衡类分布,直到多数类和少数类实现平衡。
  3. 获取更多样本

    上面的两种方法比较直接方便,但也存在弊端,比如过采样可能会导致过拟合,欠采样可能无法很好地利用有限的数据(这也可能会造成过拟合)。因此最好还是获取更多的样本来补充,我认为主要有下面两种方法:
    1. 采集

      例如在海贼王漫画的样本中,我们要进行20x20大小的海贼检测,那么为了获取尽可能多的负样本,我们可以截取一张1000x1000大小的海王类图像,将其拆分为20x20大小的片段加入到负样本中(即50x50地进行分割)。
    2. 生成

      为了获得更多负样本,我们也可将前面1000x1000的海王类图像先拆分为10x10大小,这就比之前多出了4倍的负样本图像。不过要注意的是,为了保持大小的一致,还需进一步将其拉伸至20x20的大小。 当然,其实不需要从体积上达到这么大的比例,关键是像素尺寸的匹配。
]]>
+

今天在看最小二乘回归(least squares regression)时看到作者把positive examples设成1,把negative examples设成0。感觉对这个概念既熟悉又陌生,查了一下之后一下子想起来了。在机器学习中,数据预处理一般包括数据清洗、数据集成、数据采样。而正负样本涉及到了数据采样的问题,因此后面也提一下。


正样本和负样本

简单来说,和概率论中类似,一般我们看一个问题时,只关注一个事件(希望它发生或者成功,并对其进行分析计算),而正样本就是属于我们关注的这一类别的样本,负样本就是指不属于该类别的样本。


数据采样平衡

一般来说,比如我们训练分类器时,希望样本中正负样本的比例是接近于1:1的。因为如果正样本占比很大(比如90%)或者负样本占比远超正样本,那么训练结果可想而知,获得的分类器在测试中的效果会很差。
针对这种数据不平衡的问题,有以下三种solution:

  1. 过采样(over-sampling)

    这是一种较为直接的办法,即通过随机复制少数类来增加其中的实例数量,从而可增加样本中少数类的代表性。
  2. 欠采样(under-sampling)

    这种方法也比较直接,即通过随机消除占多数类的样本来平衡类分布,直到多数类和少数类实现平衡。
  3. 获取更多样本

    上面的两种方法比较直接方便,但也存在弊端,比如过采样可能会导致过拟合,欠采样可能无法很好地利用有限的数据(这也可能会造成过拟合)。因此最好还是获取更多的样本来补充,我认为主要有下面两种方法:
    1. 采集

      例如在海贼王漫画的样本中,我们要进行20x20大小的海贼检测,那么为了获取尽可能多的负样本,我们可以截取一张1000x1000大小的海王类图像,将其拆分为20x20大小的片段加入到负样本中(即50x50地进行分割)。
    2. 生成

      为了获得更多负样本,我们也可将前面1000x1000的海王类图像先拆分为10x10大小,这就比之前多出了4倍的负样本图像。不过要注意的是,为了保持大小的一致,还需进一步将其拉伸至20x20的大小。 当然,其实不需要从体积上达到这么大的比例,关键是像素尺寸的匹配。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>今天在看最小二乘回归(least squares regression)时看到作者把positive examples设成1,把negative + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>今天在看最小二乘回归(least squares regression)时看到作者把positive examples设成1,把negative @@ -1107,13 +1107,13 @@ 2020-01-17T13:39:46.000Z 2020-01-24T10:32:53.492Z -

大家可以看看我删个文件多么曲折:

献丑了哈哈哈,这里就对这个过程中涉及到的一些问题做一个总结吧。


目录

首先我cd /进入到根目录,然后我每一步ls列出目录中的文件及子目录,一步一个脚印找到了我要删的文件——MATLAB,emmm我不想解释为什么是它。
然后我想当然的想remove掉这个文件,结果发现权限不够。这里其实可以ls -l以列表的形式查看目录中的文件及子目录并且列出每个文件拥有者、所属组、其他用户各自的权限的。
后面我又使用cd ../来回到上一级目录,这是为了怕自己搞错目录,怕删高了一级酿成惨剧。


权限

它说我没权限,于是就sudo临时给个5分钟的root权限呗。本来还想sudo su进入root的(可以用ctrl+D退出),那简直杀鸡用牛刀了。


删除文件/目录

一开始用rm,它提示我是一个目录,于是我使用了rmdir,但它的作用是删除一个空目录,而我的目录内还有文件。
于是我使用sudo rm folder_name -R即递归删除文件的方法来从里到外把这个目录中的文件都删了。
其实好像也可以sudo rm -rf folder_name强制删除,这里-r-R一样,都是递归的意思,-f就是强制执行无需确认。但是由于牢记linux最大禁忌rm -rf /*(真正的从删库到跑路),对这个命令还是比较怕的,于是就采取了前者。执行完之后再ls看了一下,发现已成功删除,df查看空间分配,内存使用也回来了不少。

补充:-R递归也有许多别的妙用,比如可以通过sudo chmod a+rw file_name -R来一次性修改一个文件夹内所有文件的权限。

]]>
+

大家可以看看我删个文件多么曲折:

献丑了哈哈哈,这里就对这个过程中涉及到的一些问题做一个总结吧。


目录

首先我cd /进入到根目录,然后我每一步ls列出目录中的文件及子目录,一步一个脚印找到了我要删的文件——MATLAB,emmm我不想解释为什么是它。
然后我想当然的想remove掉这个文件,结果发现权限不够。这里其实可以ls -l以列表的形式查看目录中的文件及子目录并且列出每个文件拥有者、所属组、其他用户各自的权限的。
后面我又使用cd ../来回到上一级目录,这是为了怕自己搞错目录,怕删高了一级酿成惨剧。


权限

它说我没权限,于是就sudo临时给个5分钟的root权限呗。本来还想sudo su进入root的(可以用ctrl+D退出),那简直杀鸡用牛刀了。


删除文件/目录

一开始用rm,它提示我是一个目录,于是我使用了rmdir,但它的作用是删除一个空目录,而我的目录内还有文件。
于是我使用sudo rm folder_name -R即递归删除文件的方法来从里到外把这个目录中的文件都删了。
其实好像也可以sudo rm -rf folder_name强制删除,这里-r-R一样,都是递归的意思,-f就是强制执行无需确认。但是由于牢记linux最大禁忌rm -rf /*(真正的从删库到跑路),对这个命令还是比较怕的,于是就采取了前者。执行完之后再ls看了一下,发现已成功删除,df查看空间分配,内存使用也回来了不少。

补充:-R递归也有许多别的妙用,比如可以通过sudo chmod a+rw file_name -R来一次性修改一个文件夹内所有文件的权限。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>大家可以看看我删个文件多么曲折:<br><img src="/ubuntu20200117213946/递归删除文件.png" title="递 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>大家可以看看我删个文件多么曲折:<br><img src="/ubuntu20200117213946/递归删除文件.png" title="递 @@ -1135,13 +1135,13 @@ 2020-01-17T01:44:01.000Z 2020-01-27T08:24:08.091Z -

ubuntu有时在用户登录后会提示有软件包更新,每次更新之后按提示重启,你就会看到一个类似于安全模式下大写的GNU GRUB(一个多操作系统启动程序),虽然这没什么问题,但是我在想能不能自主地去更新呢?

References

电子文献:
https://birdteam.net/122231
https://blog.csdn.net/a3192048/article/details/86618314


apt-get

这个有点类似于windows中的dism命令,可以用于安装、更新、卸载软件,大部分操作需要root权限,因此使用命令时别忘了授权。
首先介绍一下它的常见用法:

  1. 安装

    使用如下命令安装名为xxx的软件:

    1
    sudo apt-get install xxx
  2. 卸载

    使用如下命令卸载名为xxx的软件:

    1
    sudo apt-get remove xxx

    注意:切忌卸载关键的软件包,比如coreutils。

  3. 更新

    本文重点来了,apt-get相关升级更新命令有下面这四个:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    sudo apt-get update
    #更新软件源缓存,从服务器更新可用的软件列表,一般在安装软件时引入新的软件仓库之后使用

    sudo apt-get upgrade
    #更新系统,即根据列表更新已安装的软件包,既不会删除在列表中已经不存在了的软件,也不会安装有依赖需求但尚未安装的软件

    sudo apt-get full-upgrade
    #根据列表更新已安装的软件包,可能会为了解决软件包冲突而删除一些已安装的软件

    sudo apt-get dist-upgrade
    #更新系统版本,也是根据列表更新已安装的软件包,可能会为了解决软件包冲突而删除一些已安装的软件,不同于full-upgrade的dist-upgrade也可能会为了解决软件包依赖问题安装新的软件包

更新软件列表

当我们想自主更新软件包时,可以依次执行下面两条命令:

1
2
sudo apt-get upgrade
sudo apt-get dist-upgrade //谨慎执行

这两条命令其实比较类似,不同的是当相依性问题时,upgrade时此package就不会被升级而保留下来;而dist-upgrade相对“智能”,若遇到相依性问题,需要安装或者移除新的package时,dist-upgrade命令就会试着去安装或者移除它,这就可能以牺牲某些非重要软件包为代价来升级某些非常重要的软件包,个人认为存在一定风险。


apt

在根据各类教程安装各个软件时,我开始注意到有时候apt-get的位置被apt代替了。随着使用量的增加,这个疑惑越来越大,因此我决定搞搞清楚。
其实,apt命令是在ubuntu16.04发布时引入的。它具有更精减但足够的命令选项,而且具有更为有效的参数选项的组织方式。实际上,虽然不是一个东西,但完完全全可以认为aptapt-get是等价的,其格式语法几乎完全统一,在使用时不会出现不同。目前apt命令还在不断地发展,而apt-getapt有更多、更细化的操作功能,有时对于一些低级操作,仍需使用apt-get
下表是apt命令与apt-get等命令的对比,可以看到在普通使用时是完全一样的。

apt命令等效命令功能
apt installapt-get install安装软件包
apt removeapt-get remove移除软件包
apt purgeapt-get purge移除软件包及配置文件
apt updateapt-get update更新软件列表
apt upgradeapt-get upgrade升级所有可升级的软件包
apt autoremoveapt-get autoremove自动删除不需要的包
apt full-upgradeapt-get full-upgrade在升级软件包时自动处理依赖关系
apt searchapt-get search搜索应用程序
apt showapt-get show显示软件包信息

此外,apt还有一些自己的命令,比如apt list列出包含条件的包(已安装,可升级等);apt edit-sources编辑源列表。

]]>
+

ubuntu有时在用户登录后会提示有软件包更新,每次更新之后按提示重启,你就会看到一个类似于安全模式下大写的GNU GRUB(一个多操作系统启动程序),虽然这没什么问题,但是我在想能不能自主地去更新呢?

References

电子文献:
https://birdteam.net/122231
https://blog.csdn.net/a3192048/article/details/86618314


apt-get

这个有点类似于windows中的dism命令,可以用于安装、更新、卸载软件,大部分操作需要root权限,因此使用命令时别忘了授权。
首先介绍一下它的常见用法:

  1. 安装

    使用如下命令安装名为xxx的软件:

    1
    sudo apt-get install xxx
  2. 卸载

    使用如下命令卸载名为xxx的软件:

    1
    sudo apt-get remove xxx

    注意:切忌卸载关键的软件包,比如coreutils。

  3. 更新

    本文重点来了,apt-get相关升级更新命令有下面这四个:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    sudo apt-get update
    #更新软件源缓存,从服务器更新可用的软件列表,一般在安装软件时引入新的软件仓库之后使用

    sudo apt-get upgrade
    #更新系统,即根据列表更新已安装的软件包,既不会删除在列表中已经不存在了的软件,也不会安装有依赖需求但尚未安装的软件

    sudo apt-get full-upgrade
    #根据列表更新已安装的软件包,可能会为了解决软件包冲突而删除一些已安装的软件

    sudo apt-get dist-upgrade
    #更新系统版本,也是根据列表更新已安装的软件包,可能会为了解决软件包冲突而删除一些已安装的软件,不同于full-upgrade的dist-upgrade也可能会为了解决软件包依赖问题安装新的软件包

更新软件列表

当我们想自主更新软件包时,可以依次执行下面两条命令:

1
2
sudo apt-get upgrade
sudo apt-get dist-upgrade //谨慎执行

这两条命令其实比较类似,不同的是当相依性问题时,upgrade时此package就不会被升级而保留下来;而dist-upgrade相对“智能”,若遇到相依性问题,需要安装或者移除新的package时,dist-upgrade命令就会试着去安装或者移除它,这就可能以牺牲某些非重要软件包为代价来升级某些非常重要的软件包,个人认为存在一定风险。


apt

在根据各类教程安装各个软件时,我开始注意到有时候apt-get的位置被apt代替了。随着使用量的增加,这个疑惑越来越大,因此我决定搞搞清楚。
其实,apt命令是在ubuntu16.04发布时引入的。它具有更精减但足够的命令选项,而且具有更为有效的参数选项的组织方式。实际上,虽然不是一个东西,但完完全全可以认为aptapt-get是等价的,其格式语法几乎完全统一,在使用时不会出现不同。目前apt命令还在不断地发展,而apt-getapt有更多、更细化的操作功能,有时对于一些低级操作,仍需使用apt-get
下表是apt命令与apt-get等命令的对比,可以看到在普通使用时是完全一样的。

apt命令等效命令功能
apt installapt-get install安装软件包
apt removeapt-get remove移除软件包
apt purgeapt-get purge移除软件包及配置文件
apt updateapt-get update更新软件列表
apt upgradeapt-get upgrade升级所有可升级的软件包
apt autoremoveapt-get autoremove自动删除不需要的包
apt full-upgradeapt-get full-upgrade在升级软件包时自动处理依赖关系
apt searchapt-get search搜索应用程序
apt showapt-get show显示软件包信息

此外,apt还有一些自己的命令,比如apt list列出包含条件的包(已安装,可升级等);apt edit-sources编辑源列表。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>ubuntu有时在用户登录后会提示有软件包更新,每次更新之后按提示重启,你就会看到一个类似于安全模式下大写的GNU GRUB(一个多操作系统启动 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>ubuntu有时在用户登录后会提示有软件包更新,每次更新之后按提示重启,你就会看到一个类似于安全模式下大写的GNU GRUB(一个多操作系统启动 @@ -1163,13 +1163,13 @@ 2020-01-17T00:53:37.000Z 2020-01-19T00:26:43.668Z -

第一次在电脑加装ubuntu双系统后,就存在ubuntu比windows系统时间慢8个小时的问题。当时搞了一会好像也解决了。然而,在我重装了ubuntu系统之后(详见ubuntu笔记:重装ubuntu——记一段辛酸血泪史),这个问题又出现了。一时间得不到很好地解决,也就没管。最近强迫症犯了,花了点功夫终于搞定了,决定记录在此。

References

电子文献:
http://doc.ntp.org/4.1.1/ntpdate.htm
http://doc.ntp.org/4.1.1/ntpd.htm
https://blog.csdn.net/vic_qxz/article/details/80344855


windows、ubuntu系统时间

在windows中,系统时间的设置较为简单。而且设置后,系统时间会自动保存在bios的时钟里面,当启动计算机时,系统会自动在bios里面读取硬件时间,以保证时间不间断。
但在ubuntu linux默认情况下,系统时间和硬件时间,并不会自动同步。在ubuntu linux运行过程中,系统时间和硬件时间以异步的方式运行,互不干扰。硬件时间是靠bios电池来维持运行的,而系统时间是用CPU tick来维持的。在系统开机时,会自动从bios中取得硬件时间,设置为系统时间。
这样一来就不奇怪了,中国的时区是东八区(GMT+8),因此ubuntu每次读入的是格林威治标准时间并直接将其设置为系统时间,而windows则会加上8:00调整。
因此我解决的思路如下:考虑到windows下时间调整更方便,我就优先调整ubuntu的系统时间,将其系统时间(即本电脑的硬件时间)设为GMT+8,然后再在windows系统中取消自动添加8小时的自动调整,即让两个系统以同样的方法从硬件时间设置系统时间。


ntpd

这里先介绍一下ntpd(Network Time Protocol (NTP) daemon),如官方文档所说,它的作用是sets and maintains the system time of day in synchronism with Internet standard time servers。因此,我们可以通过ntpdate命令进行设置,其基本格式如下:

1
ntpdate [ -bBdoqsuv ] [ -a key ] [ -e authdelay ] [ -k keyfile ] [ -o version ] [ -p samples ] [ -t timeout ] server [ ... ]

这里我们不需要用到上面的这些额外选项,因此不一一介绍了。


solution

  1. 安装

    如果还没有安装ntpdate的话,可以先执行该条命令。

    1
    sudo apt-get install ntpdate

    注意:apt-get大部分操作都需要root权限,别忘了sudo赋予权限。我有一回忘记sudo了结果搞了半天不知所以…真的太蠢了。

  2. 从服务器校准时间

    这里我使用的时间服务器是time.windows.com,好像也可以用苹果的time.apple.com或者阿里云的time.pool.aliyun.com

    1
    sudo ntpdate time.windows.com

    格式参考上文。在执行之后发现有0.005秒的微小偏差,因此感觉还是比较可靠的。

  3. 把时间同步到硬件上

    同步系统时间和硬件时间,可以使用hwclock命令。

    1
    sudo hwclock --localtime --systohc

    这里的sysyohc即系统时间(sys)写到(to)硬件时间(hard clock)。
    这时ubuntu这边已经解决了,但如果重启打开windows,会发现时间快了8小时,原因之前解释过,因为自动加了8小时,所以还要作下面的调整。

  4. 调整windows

    打开windows,调整日期/时间,把时区改到:(UTC)协调世界时。如此一来,windows上的系统时间也是硬件时间了。双系统的系统时间设置方式一致,时间准确,大功告成。

    注:如果windows中时间没有问题,那就无需调整时区。总之就是先设置好ubuntu的,然后再在windows里调整,因为windows下更好调整。似乎windows会自动更正系统时间,所以经以上4步操作后过段时间需要把时区调整回来。

]]>
+

第一次在电脑加装ubuntu双系统后,就存在ubuntu比windows系统时间慢8个小时的问题。当时搞了一会好像也解决了。然而,在我重装了ubuntu系统之后(详见ubuntu笔记:重装ubuntu——记一段辛酸血泪史),这个问题又出现了。一时间得不到很好地解决,也就没管。最近强迫症犯了,花了点功夫终于搞定了,决定记录在此。

References

电子文献:
http://doc.ntp.org/4.1.1/ntpdate.htm
http://doc.ntp.org/4.1.1/ntpd.htm
https://blog.csdn.net/vic_qxz/article/details/80344855


windows、ubuntu系统时间

在windows中,系统时间的设置较为简单。而且设置后,系统时间会自动保存在bios的时钟里面,当启动计算机时,系统会自动在bios里面读取硬件时间,以保证时间不间断。
但在ubuntu linux默认情况下,系统时间和硬件时间,并不会自动同步。在ubuntu linux运行过程中,系统时间和硬件时间以异步的方式运行,互不干扰。硬件时间是靠bios电池来维持运行的,而系统时间是用CPU tick来维持的。在系统开机时,会自动从bios中取得硬件时间,设置为系统时间。
这样一来就不奇怪了,中国的时区是东八区(GMT+8),因此ubuntu每次读入的是格林威治标准时间并直接将其设置为系统时间,而windows则会加上8:00调整。
因此我解决的思路如下:考虑到windows下时间调整更方便,我就优先调整ubuntu的系统时间,将其系统时间(即本电脑的硬件时间)设为GMT+8,然后再在windows系统中取消自动添加8小时的自动调整,即让两个系统以同样的方法从硬件时间设置系统时间。


ntpd

这里先介绍一下ntpd(Network Time Protocol (NTP) daemon),如官方文档所说,它的作用是sets and maintains the system time of day in synchronism with Internet standard time servers。因此,我们可以通过ntpdate命令进行设置,其基本格式如下:

1
ntpdate [ -bBdoqsuv ] [ -a key ] [ -e authdelay ] [ -k keyfile ] [ -o version ] [ -p samples ] [ -t timeout ] server [ ... ]

这里我们不需要用到上面的这些额外选项,因此不一一介绍了。


solution

  1. 安装

    如果还没有安装ntpdate的话,可以先执行该条命令。

    1
    sudo apt-get install ntpdate

    注意:apt-get大部分操作都需要root权限,别忘了sudo赋予权限。我有一回忘记sudo了结果搞了半天不知所以…真的太蠢了。

  2. 从服务器校准时间

    这里我使用的时间服务器是time.windows.com,好像也可以用苹果的time.apple.com或者阿里云的time.pool.aliyun.com

    1
    sudo ntpdate time.windows.com

    格式参考上文。在执行之后发现有0.005秒的微小偏差,因此感觉还是比较可靠的。

  3. 把时间同步到硬件上

    同步系统时间和硬件时间,可以使用hwclock命令。

    1
    sudo hwclock --localtime --systohc

    这里的sysyohc即系统时间(sys)写到(to)硬件时间(hard clock)。
    这时ubuntu这边已经解决了,但如果重启打开windows,会发现时间快了8小时,原因之前解释过,因为自动加了8小时,所以还要作下面的调整。

  4. 调整windows

    打开windows,调整日期/时间,把时区改到:(UTC)协调世界时。如此一来,windows上的系统时间也是硬件时间了。双系统的系统时间设置方式一致,时间准确,大功告成。

    注:如果windows中时间没有问题,那就无需调整时区。总之就是先设置好ubuntu的,然后再在windows里调整,因为windows下更好调整。似乎windows会自动更正系统时间,所以经以上4步操作后过段时间需要把时区调整回来。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>第一次在电脑加装ubuntu双系统后,就存在ubuntu比windows系统时间慢8个小时的问题。当时搞了一会好像也解决了。然而,在我重装了ub + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>第一次在电脑加装ubuntu双系统后,就存在ubuntu比windows系统时间慢8个小时的问题。当时搞了一会好像也解决了。然而,在我重装了ub @@ -1191,13 +1191,13 @@ 2020-01-16T01:57:25.000Z 2020-03-12T02:12:46.327Z -

循环矩阵是我在看相关滤波时遇到的一个terminology,通过一定的了解之后发现其具有许多有用的性质。在目标跟踪领域,循环矩阵的引入对速度的提升是非常大的。关于相关滤波,由于现在了解还不够全面和深入,暂时不提及。本文主要就循环矩阵的概念和性质做一个总结。

References

电子文献:
https://www.cnblogs.com/cj-xxz/p/10323711.html
https://blog.csdn.net/shenxiaolu1984/article/details/50884830

参考文献:
[1]High-Speed Tracking with Kernelized Correlation Filters


循环矩阵(Circulant Matrices)

任意循环矩阵可以被傅里叶变换矩阵对角化。(All circulant matrices are made diagonal by the Discrete Fourier Transform (DFT), regardless of the generating vector x.)我们在文献中往往会看到这样一个变换:

下面的$X$它就是一个循环矩阵,它是由它的第一行$x=(x_{1},x_{2},…,x_{n})$的向量组每次经过一个循环移位,得到的一个循环矩阵。其中$\widehat{x}$(读作x hat)为原向量$x$的傅里叶变换;$F$是傅里叶变换矩阵,$F^{H}$表示共轭转置。
换句话说,循环矩阵$X$相似于对角阵,其特征值是$\widehat{x}$的元素。以长度为3的$x$为例,其生成的循环矩阵为:

这样的一个矩阵它有一个特别好的性质,就是能够通过它的第一行的生成向量来做来进行对角化。通过这个式子,我们能够把$X$循环矩阵进行对角化,如此把它转换到傅里叶域之后,用离散傅里叶变化来做运算时,对速度的提升是非常大的,这将在后文进一步说明。

关于这里的对角化、傅里叶变换矩阵可以看后文,在此先跳过。

这里有必要列出循环矩阵的两个重要公式,这两个性质是比较常用和有用的:

  1. 卷积

    循环矩阵乘向量等价于生成向量的逆序和该向量卷积,可进一步转化为傅里叶变换相乘。 这里$\overline{x}$表示$x$的逆序排列,$*$表示共轭。

    注意:卷积本身也包含逆序操作。此外,这里最后一个等号利用了信号与系统中的“时域卷积,频域相乘”,即时域卷积定理,它表明两信号在时域的卷积积分对应于在频域中该两信号的傅里叶变换的乘积。

  2. 相乘

    循环矩阵的乘积仍是循环矩阵,所以我们只要维护循环矩阵的第一行,就可以以较低的复杂度维护循环矩阵的乘积。 公式中最终所得的乘积也是循环矩阵,其生成向量是原生成向量对位相乘的傅里叶逆变换。

用了上述循环矩阵的性质之后,我们就可以使得原来两个矩阵相乘的时间复杂度$O(K^{3})$能够降到$O(Klog(K))$(反向傅里叶的复杂度($O(Klog(K))$)加上向量点乘的复杂度($K$)),速度的提升是非常明显的。

注:这里K表示的是矩阵的尺寸。

在非线形的情况下,当引入了核之后,也可以得到同样的一个情况。此时需要这个核满足一定的条件,它是可以具备循环矩阵的一些性质的,例如常用的高斯核、线性核都满足这个条件,因此可以直接拿来用。


傅里叶变换矩阵(DFT matrix)

关于离散傅里叶矩阵$F$这里涉及较多的数学,想看详细推导可以参考文首给出的第二个参考链接。这里把比较关键的结论部分截了过来。

$F$在这里是一个奇异矩阵(方阵且行列式等于零),它可以对任意输入向量进行傅里叶变换,这是因为傅里叶变换具有线性性。


矩阵快速幂

在本文前的第一个参考链接中,作者是用矩阵快速幂引入的,那么这里我也简单谈一下快速幂。
顾名思义,快速幂就是快速算某个数的多少次幂。
我们知道,对于任何一个整数,都能用二进制来表示。那么对于$a^{n}$,$n$也一定可以用二进制来表示。
那么问题来了,如何计算某个数较大的次幂呢?
比如计算$a^{156}$,我们可以利用除二取余、倒序排列、高位补零的方法得到$(156)_{10}=(10011100)_{2}$。
如此可以推导:

这样一来,原本要进行$156-1=155$次乘法运算,现在运算量级相当于该幂的二进制数表示中1的个数。其时间复杂度为$O(log_{2}n)$,与朴素的$O(n)$相比,效率有了极大地提高。
以上就是一般的快速幂的基本套路。相对于一般的快速幂,矩阵快速幂仅仅是把他的底数和乘数换成了矩阵形式。其主要方法就是:通过把数放到矩阵的不同位置,然后把普通递推式构造成类似于“矩阵的等比数列”,最后快速幂求解递推式。
矩阵快速幂主要用于求一个很复杂的递推式中的某一项问题。
递推矩阵(关系矩阵)的构造,也是矩阵快速幂的难点,一般是由原始的递推公式推导或者配凑得出,网上有许多ACM的赛题解答,可以看几道理解一下思路。

]]>
+

循环矩阵是我在看相关滤波时遇到的一个terminology,通过一定的了解之后发现其具有许多有用的性质。在目标跟踪领域,循环矩阵的引入对速度的提升是非常大的。关于相关滤波,由于现在了解还不够全面和深入,暂时不提及。本文主要就循环矩阵的概念和性质做一个总结。

References

电子文献:
https://www.cnblogs.com/cj-xxz/p/10323711.html
https://blog.csdn.net/shenxiaolu1984/article/details/50884830

参考文献:
[1]High-Speed Tracking with Kernelized Correlation Filters


循环矩阵(Circulant Matrices)

任意循环矩阵可以被傅里叶变换矩阵对角化。(All circulant matrices are made diagonal by the Discrete Fourier Transform (DFT), regardless of the generating vector x.)我们在文献中往往会看到这样一个变换:

下面的$X$它就是一个循环矩阵,它是由它的第一行$x=(x_{1},x_{2},…,x_{n})$的向量组每次经过一个循环移位,得到的一个循环矩阵。其中$\widehat{x}$(读作x hat)为原向量$x$的傅里叶变换;$F$是傅里叶变换矩阵,$F^{H}$表示共轭转置。
换句话说,循环矩阵$X$相似于对角阵,其特征值是$\widehat{x}$的元素。以长度为3的$x$为例,其生成的循环矩阵为:

这样的一个矩阵它有一个特别好的性质,就是能够通过它的第一行的生成向量来做来进行对角化。通过这个式子,我们能够把$X$循环矩阵进行对角化,如此把它转换到傅里叶域之后,用离散傅里叶变化来做运算时,对速度的提升是非常大的,这将在后文进一步说明。

关于这里的对角化、傅里叶变换矩阵可以看后文,在此先跳过。

这里有必要列出循环矩阵的两个重要公式,这两个性质是比较常用和有用的:

  1. 卷积

    循环矩阵乘向量等价于生成向量的逆序和该向量卷积,可进一步转化为傅里叶变换相乘。 这里$\overline{x}$表示$x$的逆序排列,$*$表示共轭。

    注意:卷积本身也包含逆序操作。此外,这里最后一个等号利用了信号与系统中的“时域卷积,频域相乘”,即时域卷积定理,它表明两信号在时域的卷积积分对应于在频域中该两信号的傅里叶变换的乘积。

  2. 相乘

    循环矩阵的乘积仍是循环矩阵,所以我们只要维护循环矩阵的第一行,就可以以较低的复杂度维护循环矩阵的乘积。 公式中最终所得的乘积也是循环矩阵,其生成向量是原生成向量对位相乘的傅里叶逆变换。

用了上述循环矩阵的性质之后,我们就可以使得原来两个矩阵相乘的时间复杂度$O(K^{3})$能够降到$O(Klog(K))$(反向傅里叶的复杂度($O(Klog(K))$)加上向量点乘的复杂度($K$)),速度的提升是非常明显的。

注:这里K表示的是矩阵的尺寸。

在非线形的情况下,当引入了核之后,也可以得到同样的一个情况。此时需要这个核满足一定的条件,它是可以具备循环矩阵的一些性质的,例如常用的高斯核、线性核都满足这个条件,因此可以直接拿来用。


傅里叶变换矩阵(DFT matrix)

关于离散傅里叶矩阵$F$这里涉及较多的数学,想看详细推导可以参考文首给出的第二个参考链接。这里把比较关键的结论部分截了过来。

$F$在这里是一个奇异矩阵(方阵且行列式等于零),它可以对任意输入向量进行傅里叶变换,这是因为傅里叶变换具有线性性。


矩阵快速幂

在本文前的第一个参考链接中,作者是用矩阵快速幂引入的,那么这里我也简单谈一下快速幂。
顾名思义,快速幂就是快速算某个数的多少次幂。
我们知道,对于任何一个整数,都能用二进制来表示。那么对于$a^{n}$,$n$也一定可以用二进制来表示。
那么问题来了,如何计算某个数较大的次幂呢?
比如计算$a^{156}$,我们可以利用除二取余、倒序排列、高位补零的方法得到$(156)_{10}=(10011100)_{2}$。
如此可以推导:

这样一来,原本要进行$156-1=155$次乘法运算,现在运算量级相当于该幂的二进制数表示中1的个数。其时间复杂度为$O(log_{2}n)$,与朴素的$O(n)$相比,效率有了极大地提高。
以上就是一般的快速幂的基本套路。相对于一般的快速幂,矩阵快速幂仅仅是把他的底数和乘数换成了矩阵形式。其主要方法就是:通过把数放到矩阵的不同位置,然后把普通递推式构造成类似于“矩阵的等比数列”,最后快速幂求解递推式。
矩阵快速幂主要用于求一个很复杂的递推式中的某一项问题。
递推矩阵(关系矩阵)的构造,也是矩阵快速幂的难点,一般是由原始的递推公式推导或者配凑得出,网上有许多ACM的赛题解答,可以看几道理解一下思路。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>循环矩阵是我在看相关滤波时遇到的一个terminology,通过一定的了解之后发现其具有许多有用的性质。在目标跟踪领域,循环矩阵的引入对速度的提 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>循环矩阵是我在看相关滤波时遇到的一个terminology,通过一定的了解之后发现其具有许多有用的性质。在目标跟踪领域,循环矩阵的引入对速度的提 @@ -1217,13 +1217,13 @@ 2020-01-16T00:47:28.000Z 2020-02-01T13:56:35.209Z -

之前在deep-learning笔记:学习率衰减与批归一化一文中,我已经对仿射变换作了简单的介绍。但这里我想提出来单独对其做一个小归纳。


应用

仿射变换在计算机科学中有丰富的运用。例如,在计算机图形学中,它可以用于在较小或较大的屏幕上显示图形内容时简单地重新缩放图形内容。此外,它也可以应用于扭曲一个图像到另一个图像平面。
另一个重要的应用是训练深层神经网络时用于扩充数据集。训练深度模型需要大量的数据。在几乎所有的情况下,模型都受益于更高的泛化性能,因为有更多的训练图像。人工生成更多数据的一种方法就是对输入数据随机应用仿射变换(数据增强)。
此外,在模型要求固定尺寸的输入时,仿射变换也是一种主要的解决方案。


仿射变换

二维仿射变换可以用下面这个公式来表示:

其中$A$是在齐次坐标系中的3x3矩阵,$x$是在齐次坐标系中$(x,y,1)$形式的向量。这个公式表示$A$将一个任意向量$x$映射到另一个向量$x’$。
一般来说,仿射变换有6个自由度。根据参数的值,它将在矩阵乘法后扭曲任何图像。变换后的图像保留了原始图像中的平行直线(考虑剪切)。本质上,满足这两个条件的任何变换都是仿射的。它保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点可以确定一个唯一的仿射变换。
下面一些特殊形式的$A$,如下图所示,从上到下分别是:缩放、平移和旋转。

上述仿射变换的一个非常有用的性质是它们是线性函数。它们保留了乘法和加法运算,并遵循叠加原理。

换言之,我们可以组合2个或更多的变换:向量加法表示平移,矩阵乘法表示线性映射,只要我们用齐次坐标表示它们。即利用这个性质,我们可以将二维仿射变换视为线性变换R和平移变换T的叠加,具体可以看一下之前的文章。
举个例子,我们可以将旋转和平移如下表示:

1
2
3
A = array([[cos(angle), -sin(angle), tx],
[sin(angle), cos(angle), ty],
[0, 0, 1]])

如果理解了的话,你会发现各种各样的变化其实还挺繁琐的。不过请放心,大多数开发人员和研究人员通常省去了编写所有这些变换的麻烦,而只需依赖优化的库来执行任务。在OpenCV中进行仿射变换非常简单,如果今后遇到的比较多再做整理。


分享

之前在artificial-intelligence笔记:吴恩达——阅读论文的建议一文中提到以后会分享几个觉得有价值的公众号的,这里就再分享一个吧。本文中的图片均来自该公众号的推文。

]]>
+

之前在deep-learning笔记:学习率衰减与批归一化一文中,我已经对仿射变换作了简单的介绍。但这里我想提出来单独对其做一个小归纳。


应用

仿射变换在计算机科学中有丰富的运用。例如,在计算机图形学中,它可以用于在较小或较大的屏幕上显示图形内容时简单地重新缩放图形内容。此外,它也可以应用于扭曲一个图像到另一个图像平面。
另一个重要的应用是训练深层神经网络时用于扩充数据集。训练深度模型需要大量的数据。在几乎所有的情况下,模型都受益于更高的泛化性能,因为有更多的训练图像。人工生成更多数据的一种方法就是对输入数据随机应用仿射变换(数据增强)。
此外,在模型要求固定尺寸的输入时,仿射变换也是一种主要的解决方案。


仿射变换

二维仿射变换可以用下面这个公式来表示:

其中$A$是在齐次坐标系中的3x3矩阵,$x$是在齐次坐标系中$(x,y,1)$形式的向量。这个公式表示$A$将一个任意向量$x$映射到另一个向量$x’$。
一般来说,仿射变换有6个自由度。根据参数的值,它将在矩阵乘法后扭曲任何图像。变换后的图像保留了原始图像中的平行直线(考虑剪切)。本质上,满足这两个条件的任何变换都是仿射的。它保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点可以确定一个唯一的仿射变换。
下面一些特殊形式的$A$,如下图所示,从上到下分别是:缩放、平移和旋转。

上述仿射变换的一个非常有用的性质是它们是线性函数。它们保留了乘法和加法运算,并遵循叠加原理。

换言之,我们可以组合2个或更多的变换:向量加法表示平移,矩阵乘法表示线性映射,只要我们用齐次坐标表示它们。即利用这个性质,我们可以将二维仿射变换视为线性变换R和平移变换T的叠加,具体可以看一下之前的文章。
举个例子,我们可以将旋转和平移如下表示:

1
2
3
A = array([[cos(angle), -sin(angle), tx],
[sin(angle), cos(angle), ty],
[0, 0, 1]])

如果理解了的话,你会发现各种各样的变化其实还挺繁琐的。不过请放心,大多数开发人员和研究人员通常省去了编写所有这些变换的麻烦,而只需依赖优化的库来执行任务。在OpenCV中进行仿射变换非常简单,如果今后遇到的比较多再做整理。


分享

之前在artificial-intelligence笔记:吴恩达——阅读论文的建议一文中提到以后会分享几个觉得有价值的公众号的,这里就再分享一个吧。本文中的图片均来自该公众号的推文。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>之前在<a href="https://gsy00517.github.io/deep-learning20191001151454/" tar + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前在<a href="https://gsy00517.github.io/deep-learning20191001151454/" tar @@ -1243,13 +1243,13 @@ 2020-01-15T14:26:41.000Z 2020-01-21T11:59:48.212Z -

因为目标追踪领域最著名的比赛VOT(Visual Object Tracking),同时也拥有一个非常重要的数据集和一套比较权威的评价指标,基于的是matlab,因此我又开始用起了matlab(这么看下来貌似matlab要成我本学期用的最多的语言了)。当我download官方的toolkit之后,按着document一路比较顺利地操作了下来,结果突然遇到一个报错,说我没有C和C++编译器。WTF?我用了这么久居然都没发现这个问题…
本以为按照提示就能很快解决,结果这个问题折腾了我一整个晚上。好吧既然被折磨得这么惨那我还是本着逢血泪必写博的原则在这里写一下吧。
不过不得不说,最后成功的时候真的还是挺爽的哈哈!

References

电子文献:
https://ww2.mathworks.cn/help/matlab/matlab_external/compiling-c-mex-files-with-mingw.html?requestedDomain=uk.mathworks.com
https://ww2.mathworks.cn/matlabcentral/fileexchange/52848-matlab-support-for-mingw-w64-c-c-compiler
https://www.cnblogs.com/Vae1990Silence/p/10102375.html
https://blog.csdn.net/fly910905/article/details/86222946


MinGW

MinGW,是Minimalist GNU for Windows的缩写。它是一个可自由使用和自由发布的Windows特定头文件和使用GNU工具集导入库的集合,允许你在GNU/Linux和Windows平台生成本地的Windows程序而不需要第三方C运行时(C Runtime)库。
当初报错的时候,我也是很诧异,因为之前使用CodeBlocks和Visual Studio的时候明明是有的。而这次在matlab中编译C/C++时怎么就找不到了。
因为之前使用CodeBlocks也有找不到的情况,我当时是重装了一遍CodeBlocks解决问题的,因此我上网开了一下有没有类似的方法。按道理来说已经装了VS是可以找到的,但好像也存在即使安装了VS、matlab还是找不到编译器的情况。可以使用mex看一下具体是哪些路径没有匹配上,似乎可以通过修改注册表的方法解决,但我没有尝试。

注:matlab调用C/C++的方式主要有两种:利用MEX技术和调用C/C++动态连接库。MEX是Matlab Executable的缩写,它是一种“可在matlab中调用的C(或Fortran)语言衍生程序”。后文中还会用到。


失败的方法

毕竟是花了一个晚上,看了mathworks上网友们的各种solution,试错了许多方法,这里记录两个貌似要成功的方法(其实最后还是失败了),或许会有参考价值。
这里有一个被许多网友强调、要注意的是:下载后的mingw文件(没错它只有15kb看上去好假)要在打开的matlab中,找到相应的下载目录,右键点击然后选择下载并安装(download and install),否则似乎会出错。

  1. 禁用IPv6

    IPv6,顾名思义,就是IP地址的第6版协议。
    我们现在用的是IPv4,它的地址是32位,总数有43亿个左右,还要减去内网专用的192、170地址段,这样一来就更少了。
    然而,IPv6的地址是128位的,大概是43亿的4次方,地址极为丰富。
    网友Kshitij Mall给出了这样一个解决思路:

    1. 首先打开控制面板中的编辑系统环境变量。
    2. 在高级选项中,点击环境变量。
    3. 在系统变量栏中添加如下两个变量:(1)variable name:“JAVA_TOOL_OPTIONS”;value:“-Djava.net.preferIPv4Stack=true”;(2)variable name:“JAVA_OPTIONS”;value:“-Djava.net.preferIPv4Stack=true”。另外根据该网友所述,安装完成后可以删去这两个环境变量。

      注意:仅输入引号内部的内容。

      java文档指示,设置jvm属性java.net.preferIPv4Stack为true时,就可以禁用IPv6。反之,若设为0,则启用。

      注:禁用设置时不需要重启系统。

      由于该网友的系统配置和我相同(MATLAB 2019a on Windows 10 system with 64 bits),于是我毫不犹豫地首先尝试了他的方法,不知道是卡进去的原因还是为何,我成功看到了协议界面(原本一直卡在附加功能管理器加载的空白界面),但是最终开始下载支持包失败。

  2. 关闭防火墙

    失败之后,我继续看评论,看到一个网友感谢另一个网友提供的solution,我非常激动,感觉自己也要跟着解决了。
    似乎好像取得了一定的进展,但是在下载第三方包的时候还是卡住了(3rt party download error),我浏览了大多数网友的评论,貌似大家基本上都卡在了这里。

成功的方法

当我快要绝望的时候,突然看到了评论区感谢三连,都是感谢同一个人的。这已经是2018年的一个回复了,看评论发现他们使用的是Windows 7系统,但我决定还是试一下,结果真的成功了,非常感谢最初的solution提供者pawan singh!
具体方法如下:

  1. 这个网址下载合适的TDM-GCC。
  2. 下载之后,create一个新的到设定的安装路径中。

    注意:根据matlab文档(文首第一个参考链接)。MinGW的安装文件夹名称不能包含空格。例如,不要使用:C:\Program Files\mingw-64。应改用:C:\mingw-64。我建议直接装在C盘下面,默认似乎也是这样,维持不变即可。

  3. 与之前修改系统变量方式类似。添加新的系统变量名为MW_MINGW64_LOC,值为MinGW-w64编译器的安装位置,于我是C:\TDM-GCC-64。最后别忘了确定设置。
  4. 在matlab命令行内执行命令:setenv('MW_MINGW64_LOC', 'path'),folder为TDM-GCC的安装位置,要加单引号。例如我是:setenv('MW_MINGW64_LOC', 'C:\TDM-GCC-64')
  5. 可以继续在命令行中执行命令:mex -setup。若没有报错,则表明成功了。

然而

英语老师说,however后面的往往是重点,那么我这里就however一下。
上面的方法问题是没有,但是使用的时候有可能会收到警告:使用的是不受支持的MinGW编译器版本。
如果没有收到这个警告,那么就万事大吉,如果有的话,能运行的话依旧还是万事大吉。
But如果真的因此而运行出错,或者看着warning心里实在不舒服的话,可以看一下我后来写的matlab笔记:MEX文件函数使用中的问题

]]>
+

因为目标追踪领域最著名的比赛VOT(Visual Object Tracking),同时也拥有一个非常重要的数据集和一套比较权威的评价指标,基于的是matlab,因此我又开始用起了matlab(这么看下来貌似matlab要成我本学期用的最多的语言了)。当我download官方的toolkit之后,按着document一路比较顺利地操作了下来,结果突然遇到一个报错,说我没有C和C++编译器。WTF?我用了这么久居然都没发现这个问题…
本以为按照提示就能很快解决,结果这个问题折腾了我一整个晚上。好吧既然被折磨得这么惨那我还是本着逢血泪必写博的原则在这里写一下吧。
不过不得不说,最后成功的时候真的还是挺爽的哈哈!

References

电子文献:
https://ww2.mathworks.cn/help/matlab/matlab_external/compiling-c-mex-files-with-mingw.html?requestedDomain=uk.mathworks.com
https://ww2.mathworks.cn/matlabcentral/fileexchange/52848-matlab-support-for-mingw-w64-c-c-compiler
https://www.cnblogs.com/Vae1990Silence/p/10102375.html
https://blog.csdn.net/fly910905/article/details/86222946


MinGW

MinGW,是Minimalist GNU for Windows的缩写。它是一个可自由使用和自由发布的Windows特定头文件和使用GNU工具集导入库的集合,允许你在GNU/Linux和Windows平台生成本地的Windows程序而不需要第三方C运行时(C Runtime)库。
当初报错的时候,我也是很诧异,因为之前使用CodeBlocks和Visual Studio的时候明明是有的。而这次在matlab中编译C/C++时怎么就找不到了。
因为之前使用CodeBlocks也有找不到的情况,我当时是重装了一遍CodeBlocks解决问题的,因此我上网开了一下有没有类似的方法。按道理来说已经装了VS是可以找到的,但好像也存在即使安装了VS、matlab还是找不到编译器的情况。可以使用mex看一下具体是哪些路径没有匹配上,似乎可以通过修改注册表的方法解决,但我没有尝试。

注:matlab调用C/C++的方式主要有两种:利用MEX技术和调用C/C++动态连接库。MEX是Matlab Executable的缩写,它是一种“可在matlab中调用的C(或Fortran)语言衍生程序”。后文中还会用到。


失败的方法

毕竟是花了一个晚上,看了mathworks上网友们的各种solution,试错了许多方法,这里记录两个貌似要成功的方法(其实最后还是失败了),或许会有参考价值。
这里有一个被许多网友强调、要注意的是:下载后的mingw文件(没错它只有15kb看上去好假)要在打开的matlab中,找到相应的下载目录,右键点击然后选择下载并安装(download and install),否则似乎会出错。

  1. 禁用IPv6

    IPv6,顾名思义,就是IP地址的第6版协议。
    我们现在用的是IPv4,它的地址是32位,总数有43亿个左右,还要减去内网专用的192、170地址段,这样一来就更少了。
    然而,IPv6的地址是128位的,大概是43亿的4次方,地址极为丰富。
    网友Kshitij Mall给出了这样一个解决思路:

    1. 首先打开控制面板中的编辑系统环境变量。
    2. 在高级选项中,点击环境变量。
    3. 在系统变量栏中添加如下两个变量:(1)variable name:“JAVA_TOOL_OPTIONS”;value:“-Djava.net.preferIPv4Stack=true”;(2)variable name:“JAVA_OPTIONS”;value:“-Djava.net.preferIPv4Stack=true”。另外根据该网友所述,安装完成后可以删去这两个环境变量。

      注意:仅输入引号内部的内容。

      java文档指示,设置jvm属性java.net.preferIPv4Stack为true时,就可以禁用IPv6。反之,若设为0,则启用。

      注:禁用设置时不需要重启系统。

      由于该网友的系统配置和我相同(MATLAB 2019a on Windows 10 system with 64 bits),于是我毫不犹豫地首先尝试了他的方法,不知道是卡进去的原因还是为何,我成功看到了协议界面(原本一直卡在附加功能管理器加载的空白界面),但是最终开始下载支持包失败。

  2. 关闭防火墙

    失败之后,我继续看评论,看到一个网友感谢另一个网友提供的solution,我非常激动,感觉自己也要跟着解决了。
    似乎好像取得了一定的进展,但是在下载第三方包的时候还是卡住了(3rt party download error),我浏览了大多数网友的评论,貌似大家基本上都卡在了这里。

成功的方法

当我快要绝望的时候,突然看到了评论区感谢三连,都是感谢同一个人的。这已经是2018年的一个回复了,看评论发现他们使用的是Windows 7系统,但我决定还是试一下,结果真的成功了,非常感谢最初的solution提供者pawan singh!
具体方法如下:

  1. 这个网址下载合适的TDM-GCC。
  2. 下载之后,create一个新的到设定的安装路径中。

    注意:根据matlab文档(文首第一个参考链接)。MinGW的安装文件夹名称不能包含空格。例如,不要使用:C:\Program Files\mingw-64。应改用:C:\mingw-64。我建议直接装在C盘下面,默认似乎也是这样,维持不变即可。

  3. 与之前修改系统变量方式类似。添加新的系统变量名为MW_MINGW64_LOC,值为MinGW-w64编译器的安装位置,于我是C:\TDM-GCC-64。最后别忘了确定设置。
  4. 在matlab命令行内执行命令:setenv('MW_MINGW64_LOC', 'path'),folder为TDM-GCC的安装位置,要加单引号。例如我是:setenv('MW_MINGW64_LOC', 'C:\TDM-GCC-64')
  5. 可以继续在命令行中执行命令:mex -setup。若没有报错,则表明成功了。

然而

英语老师说,however后面的往往是重点,那么我这里就however一下。
上面的方法问题是没有,但是使用的时候有可能会收到警告:使用的是不受支持的MinGW编译器版本。
如果没有收到这个警告,那么就万事大吉,如果有的话,能运行的话依旧还是万事大吉。
But如果真的因此而运行出错,或者看着warning心里实在不舒服的话,可以看一下我后来写的matlab笔记:MEX文件函数使用中的问题

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>因为目标追踪领域最著名的比赛VOT(Visual Object Tracking),同时也拥有一个非常重要的数据集和一套比较权威的评价指标,基于 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>因为目标追踪领域最著名的比赛VOT(Visual Object Tracking),同时也拥有一个非常重要的数据集和一套比较权威的评价指标,基于 @@ -1271,13 +1271,13 @@ 2020-01-13T12:28:51.000Z 2020-02-01T09:35:33.170Z -

在python中,Pandas可以说是最实用的库之一,它提供了非常丰富的数据读写方法。可以看一下Pandas中文网提供的Pandas参考文档中对所有I/O函数的总结。

Pandas是一个开源的,BSD许可的库,为python提供高性能、易于使用的数据结构和数据分析工具。它的使用基础是Numpy(提供高性能的矩阵运算);可以用于数据挖掘和数据分析,同时也提供数据清洗的功能。
本文就对Pandas的基本使用做一个简单的归纳,所有代码可以从上往下按顺序依次执行。

References

电子文献:
https://www.cnblogs.com/chenhuabin/p/11477076.html
https://blog.csdn.net/weixin_39791387/article/details/81487549
https://kanoki.org/2019/09/16/dataframe-visualization-with-pandas-plot/
https://pandas.pydata.org/pandas-docs/stable/getting_started/10min.html
https://blog.csdn.net/weixin_41712499/article/details/82719987


csv

这里我想介绍一下一种新的数据格式:csv。它和excel很像,但又不同于excel。csv主要有如下特点:

  1. 纯文本,使用某个字符集,比如ASCII、Unicode、EBCDIC或GB2312(简体中文环境)等;
  2. 由记录组成(典型的是每行一条记录);
  3. 每条记录被分隔符(英语:Delimiter)分隔为字段(英语:Field (computer science))(典型分隔符有逗号、分号或制表符;有时分隔符可以包括可选的空格);
  4. 每条记录都有同样的字段序列。

在Pandas的使用以及AI相关竞赛数据集、结果的存储与使用中,csv文件往往承担着主角的位置。


准备

在具体使用之前,别忘了先导入所需相应的库。

1
2
import pandas as pd
import numpy as np

可以使用pd.__version__来输出版本号,注意,这里的“__”是两个“_”,这个很容易搞错且难以发现。


两大利器

Pandas中文网首页,在介绍完Pandas之后,就重点介绍了一下Pandas的两大利器。分别是DataFrame和Series。这里我先介绍一下Seires,DataFrame在后面有更详细的操作。

  1. Series简介

    Series是一种类似于一维数组的对象,是由一组数据(各种NumPy数据类型)以及一组与之相关的数据标签(即索引)组成。仅由一组数据也可产生简单的Series对象。
    我们可以通过传入一个list的数值来创建一个Series,Pandas会创建一个默认的整数索引:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    s = pd.Series([1, 3, 5, np.nan, 6, 8])

    s
    >>> 0 1.0
    1 3.0
    2 5.0
    3 NaN
    4 6.0
    5 8.0
    dtype: float64

    注:这里用np.nan来产生NaN,但要注意的是np.nan不是一个“空”对象,即使用np.nan == np.nan来判断将返回False,np.nan的类型为基本数据类型float。
    若要对某个值进行空值判断,如对np.nan,需要用np.isnan(np.nan),此时返回为True。

    另外,也可以从字典创建Series。

  2. DataFrame简介

    DataFrame是Pandas中的一个表格型的数据结构,包含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型等),DataFrame即有行索引也有列索引,可以被看做是由Series组成的字典。
    我们可以通过传入一个numpy数组来创建一个DataFrame,如下面带有一个datetime的索引以及被标注的列:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    dates = pd.date_range('20130101', periods = 6)

    dates
    >>> DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04',
    '2013-01-05', '2013-01-06'],
    dtype = 'datetime64[ns]', freq = 'D')

    df1 = pd.DataFrame(np.random.randn(6, 4), index = dates, columns = list('ABCD'))

    df1
    >>> A B C D
    2013-01-01 0.469112 -0.282863 -1.509059 -1.135632
    2013-01-02 1.212112 -0.173215 0.119209 -1.044236
    2013-01-03 -0.861849 -2.104569 -0.494929 1.071804
    2013-01-04 0.721555 -0.706771 -1.039575 0.271860
    2013-01-05 -0.424972 0.567020 0.276232 -1.087401
    2013-01-06 -0.673690 0.113648 -1.478427 0.524988

    注:上面用pd.data_range()生成了一个时间频率freq = 'D'(即天)的日期序列。

    我们也可以通过传入一个可以转换为类Series(series-like)的字典对象来创建一个DataFrame:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    df2 = pd.DataFrame({'A': 1.,
    'B': pd.Timestamp('20130102'),
    'C': pd.Series(1, index = list(range(4)), dtype = 'float32'),
    'D': np.array([3] * 4, dtype = 'int32'),
    'E': pd.Categorical(["test", "train", "test", "train"]),
    'F': 'foo'})

    df2
    >>> A B C D E F
    0 1.0 2013-01-02 1.0 3 test foo
    1 1.0 2013-01-02 1.0 3 train foo
    2 1.0 2013-01-02 1.0 3 test foo
    3 1.0 2013-01-02 1.0 3 train foo

    这里可以使用df2.dtypes来查看不同列的数据类型。

读取

无论是txt文件还是csv文件,在Pandas中都使用read_csv()读取,当然也使用同一个方法写入到文件,那就是to_csv()方法。

1
df = pd.read_csv(abs_path) #此为绝对路径

为了提供更加多样化、可定制的功能,read_csv()方法定义了数十个参数,还在大部分参数并不常用,以下是几个比较常用的参数:

  1. filepath_or_buffer:文件所在路径,可以是一个描述路径的字符串、pathlib.Path对象、http或ftp的连接,也可以是任何可调用read()的对象。这是唯一一个必传的参数,也就是上面的abs_path。
  2. encoding:编码,字符型,通常为utf-8,如果中文读取不正常,可以将encoding设为gbk。当然,也可以直接将对应文件改成utf-8编码。
  3. header:整数或者由整数组成的列表,用来指定由哪一行或者哪几行作为列名,默认为header = 0,表示用第一列作为列名。若设置header = 0,则指定第二列作为列名。要注意的是,当指定第一行之后的数据作为列名时,前面的所有行都会被略过。也可以传递一个包含多个整数的列表给header,这样每一列就会有多个列名。如果中间某一行没有指定,那么该行会被略过。例如header = [0, 2],则原本的第二行会被省去。而当文件中没有列名一行数据时,可以传递header = None,表示不从文件数据中指定行作为列名,这时Pandas会自动生成从零开始的序列作为列名。
  4. names:接着上面的header,很快就想到是不是可以自己设置列名。names就可以用来生成一个列表,为数据额外指定列名。例如:df = pd.read_csv('abs_path, names=['第一列', '第二列', '第三列', '第四列'])

在数据读取完毕之后,我们可以使用如下代码来快速查看数据是否正确地导入了。

1
2
3
4
df.head() #看一下导入后df(DataFrame)的前几行,可在括号内输入数字来设定具体显示几行,默认5行
df.tail() #类似,查看后几行

type(df) #查看类型,DataFrame的输出应该是pandas.core.frame.DataFrame


DataFrame

DataFrame的介绍在前面的简介已经写过,这里就不赘述了。事实上,Pandas中的DataFrame的操作,有很大一部分跟numpy中的二维数组的操作是近似的。
在上面的读取处理之后,我们下面对其进行一些简单的操作:

  1. 查看

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    df.head() #上文已提及
    df.tail()

    #查看列名
    print(df.columns)

    #查看索引
    print(df.index)

    #查看各列的数据格式
    print(df.dtypes)

    #查看整个DataFrame的属性信息
    print(df.info())

    #访问对应行
    df.loc[0] #这里访问了第一行,将显示列名和对应每一列第一行的数据
    #具体有关索引请看后文
  2. 筛选

    在numpy中,我们可以这样判断一个数组中每一个数和对应数值的比较结果:

    1
    2
    a = np.array(range(10))
    a > 3

    输出将是一串布尔型(True、False)的array。
    而在DataFrame中,我们可以用类似的方法通过指定列来进行筛选:

    1
    2
    #筛选第二列中数值大于80
    df[df.第二列 > 80]

    这样就会得到只用符合条件数据的对应行的一个DataFrame。
    我们也可以使用df[(df.第一列 > 80) & (df.第二列 > 80) & (df.第三列 > 80)]来进行多条件的复杂筛选。
    此外,我们可以直接根据列名提取出一个新的表格:

    1
    new = df[['第一列', '第二列']] #new为仅由第一列和第二列组成的一个新的DataFrame
  3. 排序

    可以使用如下代码根据单列或者多列的值对数据进行排序:

    1
    2
    df.sort_values(['第二列', '第一列', '第三列'], ascending = [True, True, True])
    #使用df.sort_values(['第二列', '第一列', '第三列']).head()查看排序完后前几行的结果

    这里排序的规则是:根据设置的顺序(这里是先按第二列排),从小到大升序对所有数据进行排序。其中ascending是设置升序(默认True)和降序(False)。若仅选择单列,则无需添加[],这里[]的作用是把选择的行列转换为列表。

  4. 重命名

    如果觉得我前面取得列名称不好听,可以使用下面这个代码来改成需要的名字:

    1
    df.rename(columns = {'第一列': '好听的第一列', '第二列': '好听的第二列', '第三列': '好听的第三列', '第四列': '好听的第四列',}, inplace = True)

    这里用到了字典。

  5. 索引

    前面提到了使用索引来查看第一行,可当没有数字索引,例如我们通过df = pd.DataFrame(scores, index = ['one', 'two', 'three'])把index设为one、two、three时,df.loc[0]就失效了。因此有下面几种处理方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #访问index为“one”的行
    df.loc['one']

    #访问实实在在所谓的第几行(无论index为何)
    df.iloc[0] #注意0指的是第一行

    #ix合并了loc和iloc的功能,当索引为数字索引的时候,ix和loc是等价的
    df.ix[0] #访问第一行
    df.ix['one'] #访问“one”行,这里也指的是第一行
  6. 切片

    类似的,DataFrame也支持切片操作,但还是需要注意的。
    这里总结两种切片方式:

    1. 利用索引

      即使用df.loc[:2]ordf.ix[:2]等索引方式,这里这样的话输出为前三行。
    2. 直接切片

      这种方法只能在访问多行数据时使用,例如df[:2]将输出前两行,注意,这里比上面的方法要少一行。此外,值得强调的是,用这种方法访问单行数据是禁止的,例如不能使用df[0]来访问第一行数据。
  7. 插入

    上面的索引还有一种用途,就是可以用于插入指定index的新行。

    1
    df.loc['new_index'] = ['one', 'piece', 'is', 'true']
  8. 删除

    上面插入的那行中我说了“大秘宝是真实存在的”(海贼迷懂),下面我想把这句话所在的行删了,可以使用df.dtop()来完成。

    1
    df = df.drop('new_index')
  9. 数组

    我们可以使用df.第一列.values以array的形式输出指定列的所用值。
    基于此,我们可以使用df.第一列.value_counts()来做简单的统计,也就是对该列中每一个出现数字作频次的统计。
    我们还可以直接对DataFrame做计算,例如:df * n(n为具体数值),结果就是对表中的每一个数值都乘上对应的倍数。

  10. 元素操作

    1. map函数

      map()是python自带的方法, 可以对DataFrame某列内的元素进行操作。
      下面是一种使用实例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      def func(grade):
      if grade >= 80:
      return "A"
      elif grade >= 70:
      return "B"
      elif grade >= 60:
      return "C"
      else:
      return "D"

      df['评级'] = df.第一列.map(func)

      这样DataFrame后面会自动添加一列名为“评级”,并根据第一列来生成数据填入。

    2. apply函数

      当我们需要进行根据多列生成新的一个列的操作时,就需要用到apply。其用法简单示例如下:

      1
      df['求和'] = df.apply(lambda x: x.第一列 + x.第二列, axis = 1)
    3. applymap函数

      applymap时对dataframe中所有的数据进行操作的一个函数,非常重要。例如,我要让之前所用的score和grade都变成scroe+或者grade+,那么我就可以这样:

      1
      df.applymap(lambda x: str(x) + '+')

      如果是成绩单的话,那么这样操作之后打出来就会好看些啦,哈哈。

  11. plot

    数据可视化本来是一个非常复杂的过程,但Pandas数据帧plot函数的出现,使得创建可视化图形变得很容易。
    这个函数的具体使用可以访问文首给出的第三个参考链接,为一个印度小哥利用kaggle上的数据df.plot()做的一个非常详尽的介绍。
    有机会的话我会结合matplotlib对其做一个搬运与总结,先留个坑。
  12. 统计

    我们可以使用df.describe()对数据进行快速统计汇总。输出将包括:count、mean、std、min、25%、50%、75%和max。
    通过df.mean()我们可以按列求均值,如果想要按行求均值,可以使用df.mean(1)
  13. 高阶

    此外还有使用df.T进行转置,df.dropna(how = 'any')删除所有具有缺失值的数据,df.fillna(value = 5)填充所有缺失数据等高阶用法。详细的可以查阅前面给的参考链接10 minutes to pandas,至于更高阶的,可以看一下cookbook,不过一般还是在运用的过程中遇到需求再查找,一下子记不住那么多的。

写入

通过to_csv()可以将Pandas数据写入到文本文件中,和读取read_csv()类似,它也有几个常用参数:

  1. path_or_buf:表示路径的字符串或者文件句柄,也是必需的。例如:df.to_csv(abs_path)。要注意的是,这里如果abs_path对应的文件不存在,则会新建abs_path的同名文件后再写入,如果本来已存在该文件,则会自动清空该文件后再写入。
  2. sep:分隔符,默认为逗号。当写入txt文件时,就需要这个参数来确定数据之间的分隔符了。
  3. header:元素为字符串的列表或布尔型数据。当为列表时表示重新指定列名,当为布尔型时,表示是否写入列名。这和读取时的使用基本类似。
  4. columns:后接一个列表,用于重新指定写入文件中列的顺序。例如:df.to_csv(abs_path, columns = ['第四列', '第二列', '第三列', '第一列'])
  5. index_label:字符串或布尔型变量,设置索引列列名。原本的索引是空的,使用这个参数就可以给索引添加一个列名。如果觉得不需要添加,同时空着不好看(空的话还是会有分隔符),可以设置为False去掉(同时也将不显示分隔符)。
  6. index:布尔型,是否写入索引列,默认为True。
  7. encoding:写入时所用的编码,默认是utf-8。这个和上述的许多参数其实保持默认即可。

匿名函数

在上面DataFrame一节的最后,我用到了两个匿名函数,这里我想举个例子来简单展示一下匿名函数的使用方法,的确很好用!
当我们对一个数进行操作时,若使用函数,一般会:

1
2
def func(number):
return number + 10

这样看上去就有点费代码了,因此有下面的等价匿名函数可以替代:

1
func = lambda number: number + 10

当然假如想追求代码行数的话也不拦着你~


推荐

最近看到了DataWhale的一篇文章,也总结的挺好,在这里推荐一下。

]]>
+

在python中,Pandas可以说是最实用的库之一,它提供了非常丰富的数据读写方法。可以看一下Pandas中文网提供的Pandas参考文档中对所有I/O函数的总结。

Pandas是一个开源的,BSD许可的库,为python提供高性能、易于使用的数据结构和数据分析工具。它的使用基础是Numpy(提供高性能的矩阵运算);可以用于数据挖掘和数据分析,同时也提供数据清洗的功能。
本文就对Pandas的基本使用做一个简单的归纳,所有代码可以从上往下按顺序依次执行。

References

电子文献:
https://www.cnblogs.com/chenhuabin/p/11477076.html
https://blog.csdn.net/weixin_39791387/article/details/81487549
https://kanoki.org/2019/09/16/dataframe-visualization-with-pandas-plot/
https://pandas.pydata.org/pandas-docs/stable/getting_started/10min.html
https://blog.csdn.net/weixin_41712499/article/details/82719987


csv

这里我想介绍一下一种新的数据格式:csv。它和excel很像,但又不同于excel。csv主要有如下特点:

  1. 纯文本,使用某个字符集,比如ASCII、Unicode、EBCDIC或GB2312(简体中文环境)等;
  2. 由记录组成(典型的是每行一条记录);
  3. 每条记录被分隔符(英语:Delimiter)分隔为字段(英语:Field (computer science))(典型分隔符有逗号、分号或制表符;有时分隔符可以包括可选的空格);
  4. 每条记录都有同样的字段序列。

在Pandas的使用以及AI相关竞赛数据集、结果的存储与使用中,csv文件往往承担着主角的位置。


准备

在具体使用之前,别忘了先导入所需相应的库。

1
2
import pandas as pd
import numpy as np

可以使用pd.__version__来输出版本号,注意,这里的“__”是两个“_”,这个很容易搞错且难以发现。


两大利器

Pandas中文网首页,在介绍完Pandas之后,就重点介绍了一下Pandas的两大利器。分别是DataFrame和Series。这里我先介绍一下Seires,DataFrame在后面有更详细的操作。

  1. Series简介

    Series是一种类似于一维数组的对象,是由一组数据(各种NumPy数据类型)以及一组与之相关的数据标签(即索引)组成。仅由一组数据也可产生简单的Series对象。
    我们可以通过传入一个list的数值来创建一个Series,Pandas会创建一个默认的整数索引:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    s = pd.Series([1, 3, 5, np.nan, 6, 8])

    s
    >>> 0 1.0
    1 3.0
    2 5.0
    3 NaN
    4 6.0
    5 8.0
    dtype: float64

    注:这里用np.nan来产生NaN,但要注意的是np.nan不是一个“空”对象,即使用np.nan == np.nan来判断将返回False,np.nan的类型为基本数据类型float。
    若要对某个值进行空值判断,如对np.nan,需要用np.isnan(np.nan),此时返回为True。

    另外,也可以从字典创建Series。

  2. DataFrame简介

    DataFrame是Pandas中的一个表格型的数据结构,包含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型等),DataFrame即有行索引也有列索引,可以被看做是由Series组成的字典。
    我们可以通过传入一个numpy数组来创建一个DataFrame,如下面带有一个datetime的索引以及被标注的列:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    dates = pd.date_range('20130101', periods = 6)

    dates
    >>> DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04',
    '2013-01-05', '2013-01-06'],
    dtype = 'datetime64[ns]', freq = 'D')

    df1 = pd.DataFrame(np.random.randn(6, 4), index = dates, columns = list('ABCD'))

    df1
    >>> A B C D
    2013-01-01 0.469112 -0.282863 -1.509059 -1.135632
    2013-01-02 1.212112 -0.173215 0.119209 -1.044236
    2013-01-03 -0.861849 -2.104569 -0.494929 1.071804
    2013-01-04 0.721555 -0.706771 -1.039575 0.271860
    2013-01-05 -0.424972 0.567020 0.276232 -1.087401
    2013-01-06 -0.673690 0.113648 -1.478427 0.524988

    注:上面用pd.data_range()生成了一个时间频率freq = 'D'(即天)的日期序列。

    我们也可以通过传入一个可以转换为类Series(series-like)的字典对象来创建一个DataFrame:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    df2 = pd.DataFrame({'A': 1.,
    'B': pd.Timestamp('20130102'),
    'C': pd.Series(1, index = list(range(4)), dtype = 'float32'),
    'D': np.array([3] * 4, dtype = 'int32'),
    'E': pd.Categorical(["test", "train", "test", "train"]),
    'F': 'foo'})

    df2
    >>> A B C D E F
    0 1.0 2013-01-02 1.0 3 test foo
    1 1.0 2013-01-02 1.0 3 train foo
    2 1.0 2013-01-02 1.0 3 test foo
    3 1.0 2013-01-02 1.0 3 train foo

    这里可以使用df2.dtypes来查看不同列的数据类型。

读取

无论是txt文件还是csv文件,在Pandas中都使用read_csv()读取,当然也使用同一个方法写入到文件,那就是to_csv()方法。

1
df = pd.read_csv(abs_path) #此为绝对路径

为了提供更加多样化、可定制的功能,read_csv()方法定义了数十个参数,还在大部分参数并不常用,以下是几个比较常用的参数:

  1. filepath_or_buffer:文件所在路径,可以是一个描述路径的字符串、pathlib.Path对象、http或ftp的连接,也可以是任何可调用read()的对象。这是唯一一个必传的参数,也就是上面的abs_path。
  2. encoding:编码,字符型,通常为utf-8,如果中文读取不正常,可以将encoding设为gbk。当然,也可以直接将对应文件改成utf-8编码。
  3. header:整数或者由整数组成的列表,用来指定由哪一行或者哪几行作为列名,默认为header = 0,表示用第一列作为列名。若设置header = 0,则指定第二列作为列名。要注意的是,当指定第一行之后的数据作为列名时,前面的所有行都会被略过。也可以传递一个包含多个整数的列表给header,这样每一列就会有多个列名。如果中间某一行没有指定,那么该行会被略过。例如header = [0, 2],则原本的第二行会被省去。而当文件中没有列名一行数据时,可以传递header = None,表示不从文件数据中指定行作为列名,这时Pandas会自动生成从零开始的序列作为列名。
  4. names:接着上面的header,很快就想到是不是可以自己设置列名。names就可以用来生成一个列表,为数据额外指定列名。例如:df = pd.read_csv('abs_path, names=['第一列', '第二列', '第三列', '第四列'])

在数据读取完毕之后,我们可以使用如下代码来快速查看数据是否正确地导入了。

1
2
3
4
df.head() #看一下导入后df(DataFrame)的前几行,可在括号内输入数字来设定具体显示几行,默认5行
df.tail() #类似,查看后几行

type(df) #查看类型,DataFrame的输出应该是pandas.core.frame.DataFrame


DataFrame

DataFrame的介绍在前面的简介已经写过,这里就不赘述了。事实上,Pandas中的DataFrame的操作,有很大一部分跟numpy中的二维数组的操作是近似的。
在上面的读取处理之后,我们下面对其进行一些简单的操作:

  1. 查看

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    df.head() #上文已提及
    df.tail()

    #查看列名
    print(df.columns)

    #查看索引
    print(df.index)

    #查看各列的数据格式
    print(df.dtypes)

    #查看整个DataFrame的属性信息
    print(df.info())

    #访问对应行
    df.loc[0] #这里访问了第一行,将显示列名和对应每一列第一行的数据
    #具体有关索引请看后文
  2. 筛选

    在numpy中,我们可以这样判断一个数组中每一个数和对应数值的比较结果:

    1
    2
    a = np.array(range(10))
    a > 3

    输出将是一串布尔型(True、False)的array。
    而在DataFrame中,我们可以用类似的方法通过指定列来进行筛选:

    1
    2
    #筛选第二列中数值大于80
    df[df.第二列 > 80]

    这样就会得到只用符合条件数据的对应行的一个DataFrame。
    我们也可以使用df[(df.第一列 > 80) & (df.第二列 > 80) & (df.第三列 > 80)]来进行多条件的复杂筛选。
    此外,我们可以直接根据列名提取出一个新的表格:

    1
    new = df[['第一列', '第二列']] #new为仅由第一列和第二列组成的一个新的DataFrame
  3. 排序

    可以使用如下代码根据单列或者多列的值对数据进行排序:

    1
    2
    df.sort_values(['第二列', '第一列', '第三列'], ascending = [True, True, True])
    #使用df.sort_values(['第二列', '第一列', '第三列']).head()查看排序完后前几行的结果

    这里排序的规则是:根据设置的顺序(这里是先按第二列排),从小到大升序对所有数据进行排序。其中ascending是设置升序(默认True)和降序(False)。若仅选择单列,则无需添加[],这里[]的作用是把选择的行列转换为列表。

  4. 重命名

    如果觉得我前面取得列名称不好听,可以使用下面这个代码来改成需要的名字:

    1
    df.rename(columns = {'第一列': '好听的第一列', '第二列': '好听的第二列', '第三列': '好听的第三列', '第四列': '好听的第四列',}, inplace = True)

    这里用到了字典。

  5. 索引

    前面提到了使用索引来查看第一行,可当没有数字索引,例如我们通过df = pd.DataFrame(scores, index = ['one', 'two', 'three'])把index设为one、two、three时,df.loc[0]就失效了。因此有下面几种处理方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #访问index为“one”的行
    df.loc['one']

    #访问实实在在所谓的第几行(无论index为何)
    df.iloc[0] #注意0指的是第一行

    #ix合并了loc和iloc的功能,当索引为数字索引的时候,ix和loc是等价的
    df.ix[0] #访问第一行
    df.ix['one'] #访问“one”行,这里也指的是第一行
  6. 切片

    类似的,DataFrame也支持切片操作,但还是需要注意的。
    这里总结两种切片方式:

    1. 利用索引

      即使用df.loc[:2]ordf.ix[:2]等索引方式,这里这样的话输出为前三行。
    2. 直接切片

      这种方法只能在访问多行数据时使用,例如df[:2]将输出前两行,注意,这里比上面的方法要少一行。此外,值得强调的是,用这种方法访问单行数据是禁止的,例如不能使用df[0]来访问第一行数据。
  7. 插入

    上面的索引还有一种用途,就是可以用于插入指定index的新行。

    1
    df.loc['new_index'] = ['one', 'piece', 'is', 'true']
  8. 删除

    上面插入的那行中我说了“大秘宝是真实存在的”(海贼迷懂),下面我想把这句话所在的行删了,可以使用df.dtop()来完成。

    1
    df = df.drop('new_index')
  9. 数组

    我们可以使用df.第一列.values以array的形式输出指定列的所用值。
    基于此,我们可以使用df.第一列.value_counts()来做简单的统计,也就是对该列中每一个出现数字作频次的统计。
    我们还可以直接对DataFrame做计算,例如:df * n(n为具体数值),结果就是对表中的每一个数值都乘上对应的倍数。

  10. 元素操作

    1. map函数

      map()是python自带的方法, 可以对DataFrame某列内的元素进行操作。
      下面是一种使用实例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      def func(grade):
      if grade >= 80:
      return "A"
      elif grade >= 70:
      return "B"
      elif grade >= 60:
      return "C"
      else:
      return "D"

      df['评级'] = df.第一列.map(func)

      这样DataFrame后面会自动添加一列名为“评级”,并根据第一列来生成数据填入。

    2. apply函数

      当我们需要进行根据多列生成新的一个列的操作时,就需要用到apply。其用法简单示例如下:

      1
      df['求和'] = df.apply(lambda x: x.第一列 + x.第二列, axis = 1)
    3. applymap函数

      applymap时对dataframe中所有的数据进行操作的一个函数,非常重要。例如,我要让之前所用的score和grade都变成scroe+或者grade+,那么我就可以这样:

      1
      df.applymap(lambda x: str(x) + '+')

      如果是成绩单的话,那么这样操作之后打出来就会好看些啦,哈哈。

  11. plot

    数据可视化本来是一个非常复杂的过程,但Pandas数据帧plot函数的出现,使得创建可视化图形变得很容易。
    这个函数的具体使用可以访问文首给出的第三个参考链接,为一个印度小哥利用kaggle上的数据df.plot()做的一个非常详尽的介绍。
    有机会的话我会结合matplotlib对其做一个搬运与总结,先留个坑。
  12. 统计

    我们可以使用df.describe()对数据进行快速统计汇总。输出将包括:count、mean、std、min、25%、50%、75%和max。
    通过df.mean()我们可以按列求均值,如果想要按行求均值,可以使用df.mean(1)
  13. 高阶

    此外还有使用df.T进行转置,df.dropna(how = 'any')删除所有具有缺失值的数据,df.fillna(value = 5)填充所有缺失数据等高阶用法。详细的可以查阅前面给的参考链接10 minutes to pandas,至于更高阶的,可以看一下cookbook,不过一般还是在运用的过程中遇到需求再查找,一下子记不住那么多的。

写入

通过to_csv()可以将Pandas数据写入到文本文件中,和读取read_csv()类似,它也有几个常用参数:

  1. path_or_buf:表示路径的字符串或者文件句柄,也是必需的。例如:df.to_csv(abs_path)。要注意的是,这里如果abs_path对应的文件不存在,则会新建abs_path的同名文件后再写入,如果本来已存在该文件,则会自动清空该文件后再写入。
  2. sep:分隔符,默认为逗号。当写入txt文件时,就需要这个参数来确定数据之间的分隔符了。
  3. header:元素为字符串的列表或布尔型数据。当为列表时表示重新指定列名,当为布尔型时,表示是否写入列名。这和读取时的使用基本类似。
  4. columns:后接一个列表,用于重新指定写入文件中列的顺序。例如:df.to_csv(abs_path, columns = ['第四列', '第二列', '第三列', '第一列'])
  5. index_label:字符串或布尔型变量,设置索引列列名。原本的索引是空的,使用这个参数就可以给索引添加一个列名。如果觉得不需要添加,同时空着不好看(空的话还是会有分隔符),可以设置为False去掉(同时也将不显示分隔符)。
  6. index:布尔型,是否写入索引列,默认为True。
  7. encoding:写入时所用的编码,默认是utf-8。这个和上述的许多参数其实保持默认即可。

匿名函数

在上面DataFrame一节的最后,我用到了两个匿名函数,这里我想举个例子来简单展示一下匿名函数的使用方法,的确很好用!
当我们对一个数进行操作时,若使用函数,一般会:

1
2
def func(number):
return number + 10

这样看上去就有点费代码了,因此有下面的等价匿名函数可以替代:

1
func = lambda number: number + 10

当然假如想追求代码行数的话也不拦着你~


推荐

最近看到了DataWhale的一篇文章,也总结的挺好,在这里推荐一下。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>在python中,Pandas可以说是最实用的库之一,它提供了非常丰富的数据读写方法。可以看一下Pandas中文网提供的<a href="htt + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在python中,Pandas可以说是最实用的库之一,它提供了非常丰富的数据读写方法。可以看一下Pandas中文网提供的<a href="htt @@ -1299,13 +1299,13 @@ 2020-01-13T09:47:31.000Z 2020-02-15T14:10:22.066Z -

实践出真知。在之前的博文deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现中,我对何恺明大神的CVPR最佳论文中提出的残差网络做了简单介绍。而就在第二年(2016年),何恺明的团队就发表了“Identity Mappings in Deep Residual Networks”这篇文章,分析了ResNet成功的关键因素——residual block背后的算法,并对residual block以及after-addition activation进行改进,通过一系列的ablation experiments验证了,在residual block和after-addition activation上都使用identity mapping(恒等映射)时,能对模型训练产生很好的效果。不知道为什么,我今天从arXiv上download这篇paper的时候发现上不去了,莫非现在上arXiv也要科学上网了?
本次实战主要是基于之前的ResNet实现和flyAI平台,并结合上面提到的何恺明团队分析ResNet的论文做出一些改进,并检验效果。

References

电子文献:
https://blog.csdn.net/Sandwichsauce/article/details/89162570
https://www.jianshu.com/p/184799230f20
https://blog.csdn.net/wspba/article/details/60572886
https://www.cnblogs.com/4991tcl/p/10395574.html
https://blog.csdn.net/DuinoDu/article/details/80435127

参考文献:
[1]Identity Mappings in Deep Residual Networks


ablation experiments

在上面我提到了这个名词,中文翻译是“消融实验”。或许在阅读论文的过程中会接触到这个名词,如果仅根据字面翻译的话或许会很纳闷。
在查找了一定的资料后,我对这种方法有了大致地了解。
ablation的原本释义是通过机械方法切除身体组织,如手术,从身体中去除尤指器官以及异常生长的有害物质。
事实上,这种方法类似于物理实验中的控制变量法,即当在一个新提出的模型中同时改变了多个条件或者参数,那么为了分析和检验,在接下去的消融实验中,会一一控制每个条件或者参数不变,来根据结果分析到底是哪个条件或者参数对模型的优化、影响更大。
在机器学习、特别是复杂的深度神经网络的背景下,科研工作者们已经采用“消融研究”来描述去除网络的某些部分的过程,以便更好地理解网络的行为。


ResNet的分析与改进

  1. 残差单元

    在2015年ResNet首次发布的时候,设计的残差单元在最后的输出之前是要经过一个激活函数的。而在2016年新提出的残差单元中,去掉了这个激活函数,并通过实验证明新提出的残差单元训练更简单。 这种新的构造的关键在于不仅仅是在残差单元的内部,而是在整个网络中创建一个“直接”的计算传播路径来分析深度残差网络。通过构造这样一个“干净”的信息通路,可以在前向和反向传播阶段,使信号能够直接的从一个单元传递到其他任意一个单元。实验表明,当框架接近于上面的状态时,训练会变得更加简单。
  2. shortcut

    对于恒等跳跃连接$h(x_{l})=x_{l}$,作者设计了5种新的连接方式来与原本的方式作对比,设计以及实验结果如下所示: 其中fail表示测试误差超过了20%。实验结果表明,原本的连接方式误差衰减最快,同时误差也最低,而其他形式的shortcut都产生了较大的损失和误差。
    作者认为,shortcut连接中的操作 (缩放、门控、1×1的卷积以及dropout) 会阻碍信息的传递,以致于对优化造成困难。
    此外,虽然1×1的卷积shortcut连接引入了更多的参数,本应该比恒等shortcut连接具有更加强大的表达能力。但是它的效果并不好,这表明了这些模型退化问题的原因是优化问题,而不是表达能力的问题。
  3. 激活函数

    对于激活函数的设置,作者设计了如下几种方式进行比较: 在这里,作者将激活项分为了预激活(pre-activation)和后激活(post-activation)。通过实验可以发现,将ReLU和BN都放在预激活中,即full pre-activation最为有效。

ResNet实战

根据论文中的实验结果,我使用了新的残差模块进行实践。并结合在deep-learning笔记:学习率衰减与批归一化中的分析总结对BN层的位置选取作了简单调整。在本次实验中,我尝试使用了StepLR阶梯式衰减和连续衰减两种学习率衰减方式,事实证明,使用StepLR阶梯式衰减的效果在这里要略好一些(连续衰减前期学得太快,后面大半部分都学不动了…)。
首次训练的结果并不理想,于是我加大了学习率每次衰减的幅度,即让最后阶段的学习率更小,这使我的模型的评分提高了不少。
由于训练资源有限,我没能进行更深(仅设置了10层左右)、更久(每次仅进行20个epoch)的训练,但在每个batch中,最高的accuracy也能达到65%左右,平均大约能超过50%。相比之前使用浅层网络仅能达到20%左右的accuracy,这已经提升不少了。然而最终的打分还是没有显著提高,因此我思考是否存在过拟合的问题。为此我尝试着在全连接层和捷径连接中加入dropout正则化来提高在测试集中的泛化能力,结果最终打分仅提高了0.1,而训练时间稍短。由于我除了dropout之外并没有改变网络的层数等影响参数量的因素,因此似乎与何大神在论文中original版和dropout版shortchut的比较有一些矛盾,但的确还是说明了dropout在这里的作用微乎其微,优化模型时可以排除至考虑范围之外了。


遇到的问题

  1. TabError: inconsistent use of tabs and spaces in indentation

    当我在flyAI提供的窗口中修改代码并提交GPU训练时,就出现了这个报错。它说我在缩进时错误的使用了制表符和空格。于是我只好把报错处的缩进删除并重敲tab缩进,问题就得到了解决。
    如果使用PyCharm等IDE的话,这个错误会直接显示出来,即在缩进处会有灰色的颜色警告,将光标移过去就会有具体报错。这就省得提交GPU之后才能收到报错,所以以后写代码、改代码能用IDE还是用起来好啦。
  2. RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation

    这是在shortcut残差连接时遇到的一个报错,上网后发现原因很简单:版本问题。在新版的pytorch中,由于0.4.0之后把Varible和Tensor融合为一个Tensor,因此inplace操作在之前对Varible时还能用,但现在只有Tensor,就会出错了。
    解决的办法是将x += self.shortcut(x1)替换成x = x + self.shortcut(x1)
    若网络很大,找起来很麻烦,可以在网络的中间变量加一句x.backward(),看会不会报错,如果不会的话,那就说明至少这之前是没毛病的。
  3. 张量第一维是batch size

    起初,我根据输入的torch.Size([64, 1, 128, 128]),使用如下函数将输出拍平成1维的:

    1
    2
    3
    4
    5
    6
    def num_flat_features(self, x):
    size = x.size()[0:]
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    同时,为了匹配,我将第一个全连接层的输入乘上了64。其实这个时候我已经开始怀疑这个64是哪来的了,为什么这个张量第一维尺度有64。
    直到后来平台报错,我才意识到这个表示的不是数据的维度,而是我设计的batch size。
    为此我将上面的代码调整如下:

    1
    2
    3
    4
    5
    6
    def num_flat_features(self, x):
    size = x.size()[1:]
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    如此,问题得到解决,最终的输出应该是batch size乘上总类别数的一个张量。


arXiv

文前提到了上arXiv下论文要科学上网的事情,后来我发现了一个中科院理论物理所的一个备选镜像,但是好像不是特别稳定,不过还是先留在这里吧,万一的话可以拿来试试。
一般一些科研工作者会在论文发布之前上传到arXiv以防止自己的idea被别人用了。估计主要是为了防止类似牛顿莱布尼兹之争这种事吧。

]]>
+

实践出真知。在之前的博文deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现中,我对何恺明大神的CVPR最佳论文中提出的残差网络做了简单介绍。而就在第二年(2016年),何恺明的团队就发表了“Identity Mappings in Deep Residual Networks”这篇文章,分析了ResNet成功的关键因素——residual block背后的算法,并对residual block以及after-addition activation进行改进,通过一系列的ablation experiments验证了,在residual block和after-addition activation上都使用identity mapping(恒等映射)时,能对模型训练产生很好的效果。不知道为什么,我今天从arXiv上download这篇paper的时候发现上不去了,莫非现在上arXiv也要科学上网了?
本次实战主要是基于之前的ResNet实现和flyAI平台,并结合上面提到的何恺明团队分析ResNet的论文做出一些改进,并检验效果。

References

电子文献:
https://blog.csdn.net/Sandwichsauce/article/details/89162570
https://www.jianshu.com/p/184799230f20
https://blog.csdn.net/wspba/article/details/60572886
https://www.cnblogs.com/4991tcl/p/10395574.html
https://blog.csdn.net/DuinoDu/article/details/80435127

参考文献:
[1]Identity Mappings in Deep Residual Networks


ablation experiments

在上面我提到了这个名词,中文翻译是“消融实验”。或许在阅读论文的过程中会接触到这个名词,如果仅根据字面翻译的话或许会很纳闷。
在查找了一定的资料后,我对这种方法有了大致地了解。
ablation的原本释义是通过机械方法切除身体组织,如手术,从身体中去除尤指器官以及异常生长的有害物质。
事实上,这种方法类似于物理实验中的控制变量法,即当在一个新提出的模型中同时改变了多个条件或者参数,那么为了分析和检验,在接下去的消融实验中,会一一控制每个条件或者参数不变,来根据结果分析到底是哪个条件或者参数对模型的优化、影响更大。
在机器学习、特别是复杂的深度神经网络的背景下,科研工作者们已经采用“消融研究”来描述去除网络的某些部分的过程,以便更好地理解网络的行为。


ResNet的分析与改进

  1. 残差单元

    在2015年ResNet首次发布的时候,设计的残差单元在最后的输出之前是要经过一个激活函数的。而在2016年新提出的残差单元中,去掉了这个激活函数,并通过实验证明新提出的残差单元训练更简单。 这种新的构造的关键在于不仅仅是在残差单元的内部,而是在整个网络中创建一个“直接”的计算传播路径来分析深度残差网络。通过构造这样一个“干净”的信息通路,可以在前向和反向传播阶段,使信号能够直接的从一个单元传递到其他任意一个单元。实验表明,当框架接近于上面的状态时,训练会变得更加简单。
  2. shortcut

    对于恒等跳跃连接$h(x_{l})=x_{l}$,作者设计了5种新的连接方式来与原本的方式作对比,设计以及实验结果如下所示: 其中fail表示测试误差超过了20%。实验结果表明,原本的连接方式误差衰减最快,同时误差也最低,而其他形式的shortcut都产生了较大的损失和误差。
    作者认为,shortcut连接中的操作 (缩放、门控、1×1的卷积以及dropout) 会阻碍信息的传递,以致于对优化造成困难。
    此外,虽然1×1的卷积shortcut连接引入了更多的参数,本应该比恒等shortcut连接具有更加强大的表达能力。但是它的效果并不好,这表明了这些模型退化问题的原因是优化问题,而不是表达能力的问题。
  3. 激活函数

    对于激活函数的设置,作者设计了如下几种方式进行比较: 在这里,作者将激活项分为了预激活(pre-activation)和后激活(post-activation)。通过实验可以发现,将ReLU和BN都放在预激活中,即full pre-activation最为有效。

ResNet实战

根据论文中的实验结果,我使用了新的残差模块进行实践。并结合在deep-learning笔记:学习率衰减与批归一化中的分析总结对BN层的位置选取作了简单调整。在本次实验中,我尝试使用了StepLR阶梯式衰减和连续衰减两种学习率衰减方式,事实证明,使用StepLR阶梯式衰减的效果在这里要略好一些(连续衰减前期学得太快,后面大半部分都学不动了…)。
首次训练的结果并不理想,于是我加大了学习率每次衰减的幅度,即让最后阶段的学习率更小,这使我的模型的评分提高了不少。
由于训练资源有限,我没能进行更深(仅设置了10层左右)、更久(每次仅进行20个epoch)的训练,但在每个batch中,最高的accuracy也能达到65%左右,平均大约能超过50%。相比之前使用浅层网络仅能达到20%左右的accuracy,这已经提升不少了。然而最终的打分还是没有显著提高,因此我思考是否存在过拟合的问题。为此我尝试着在全连接层和捷径连接中加入dropout正则化来提高在测试集中的泛化能力,结果最终打分仅提高了0.1,而训练时间稍短。由于我除了dropout之外并没有改变网络的层数等影响参数量的因素,因此似乎与何大神在论文中original版和dropout版shortchut的比较有一些矛盾,但的确还是说明了dropout在这里的作用微乎其微,优化模型时可以排除至考虑范围之外了。


遇到的问题

  1. TabError: inconsistent use of tabs and spaces in indentation

    当我在flyAI提供的窗口中修改代码并提交GPU训练时,就出现了这个报错。它说我在缩进时错误的使用了制表符和空格。于是我只好把报错处的缩进删除并重敲tab缩进,问题就得到了解决。
    如果使用PyCharm等IDE的话,这个错误会直接显示出来,即在缩进处会有灰色的颜色警告,将光标移过去就会有具体报错。这就省得提交GPU之后才能收到报错,所以以后写代码、改代码能用IDE还是用起来好啦。
  2. RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation

    这是在shortcut残差连接时遇到的一个报错,上网后发现原因很简单:版本问题。在新版的pytorch中,由于0.4.0之后把Varible和Tensor融合为一个Tensor,因此inplace操作在之前对Varible时还能用,但现在只有Tensor,就会出错了。
    解决的办法是将x += self.shortcut(x1)替换成x = x + self.shortcut(x1)
    若网络很大,找起来很麻烦,可以在网络的中间变量加一句x.backward(),看会不会报错,如果不会的话,那就说明至少这之前是没毛病的。
  3. 张量第一维是batch size

    起初,我根据输入的torch.Size([64, 1, 128, 128]),使用如下函数将输出拍平成1维的:

    1
    2
    3
    4
    5
    6
    def num_flat_features(self, x):
    size = x.size()[0:]
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    同时,为了匹配,我将第一个全连接层的输入乘上了64。其实这个时候我已经开始怀疑这个64是哪来的了,为什么这个张量第一维尺度有64。
    直到后来平台报错,我才意识到这个表示的不是数据的维度,而是我设计的batch size。
    为此我将上面的代码调整如下:

    1
    2
    3
    4
    5
    6
    def num_flat_features(self, x):
    size = x.size()[1:]
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    如此,问题得到解决,最终的输出应该是batch size乘上总类别数的一个张量。


arXiv

文前提到了上arXiv下论文要科学上网的事情,后来我发现了一个中科院理论物理所的一个备选镜像,但是好像不是特别稳定,不过还是先留在这里吧,万一的话可以拿来试试。
一般一些科研工作者会在论文发布之前上传到arXiv以防止自己的idea被别人用了。估计主要是为了防止类似牛顿莱布尼兹之争这种事吧。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>实践出真知。在之前的博文<a href="https://gsy00517.github.io/deep-learning20191001184 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>实践出真知。在之前的博文<a href="https://gsy00517.github.io/deep-learning20191001184 @@ -1314,12 +1314,12 @@ + + - - @@ -1331,13 +1331,13 @@ 2020-01-08T13:40:52.000Z 2020-02-07T09:36:02.928Z -

在我们训练模型的时候,我们总希望能够直接看到训练的进度,下面我就总结几个我收集的打印进度的方法。

References

电子文献:
https://blog.csdn.net/u013985241/article/details/86653356
https://blog.csdn.net/zkp_987/article/details/81748098


利用回车符

打印百分比应该是最常见的方法,也是我一直使用的。不过如果简单地逐次打印百分比的话,就会占据大量的屏幕空间,甚至装不下而需要手动拖动滚动条,让人眼花缭乱。这时我就想到了利用转义符“\r”,在print完本次的进度之后,下一次直接回车将其清除覆盖,这样就达到了既不占用屏幕又清晰的目的。
大致的方法如下:

1
2
3
4
5
import time #这里是为了用来延时,代替训练的时间
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
for i in range(numOfTimes):
print("\r", "progress percentage:{0}%".format((round(i + 1) * 100 / numOfTimes)), end = "", flush = True)
time.sleep(0.02) #若前面from time import sleep,这里直接sleep(0.02)即可

这里用到了python的format格式化函数,format中计算出的数值对应的位置是{0},将在实际print的过程中被替换。
此外,这里还用到了round()函数,其作用是返回浮点数的四舍五入值。
关于上面在print()函数中出现的flush,文首的参考链接中已给出解释,这里做个搬运:
因为print()函数会把内容放到内存中,内存中的内容并不一定能够及时刷新显示到屏幕中。而当我们使用flush = True之后,会在print结束之后,立即将内存中的东西显示到屏幕上,清空缓存。
基于上述原理,flush大致有下面两个使用场景:

  1. 在循环中,要想每进行一次循环体,在屏幕上更新打印的内容就得使用flush = True的参数设置。(我这里就是这种情况)
  2. 打开一个文件,向其写入字符串,在关闭文件f.close()之前 打开文件是看不到写入的字符的。因此,如果要想在关闭之前实时地看到写入的字符串,那么就应该使用flush = True

利用tqdm库

有需求就有市场,一搜果然还是有库能满足我的需求的。tqdm就是其中之一,它是一个快速,可扩展的python进度条,可以在python长循环中添加一个进度提示信息。
大致用法如下:

1
2
3
4
5
6
import tqdm
import time
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
for i in tqdm.tqdm(range(numOfTimes)):
time.sleep(0.02) #代替训练等耗时过程
pass

也可以直接from tqdm import tqdm,这样后面就不需要tqdm.tqdm了。


利用progressbar

库如其名,这个库就是用来做进度条的。如果没有的话,它和tqdm都可以使用pip来安装。

1
2
3
4
5
6
import progressbar
from time import sleep
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
progress = progressbar.ProgressBar()
for i in progress(range(numOfTimes)):
sleep(0.02)

]]>
+

在我们训练模型的时候,我们总希望能够直接看到训练的进度,下面我就总结几个我收集的打印进度的方法。

References

电子文献:
https://blog.csdn.net/u013985241/article/details/86653356
https://blog.csdn.net/zkp_987/article/details/81748098


利用回车符

打印百分比应该是最常见的方法,也是我一直使用的。不过如果简单地逐次打印百分比的话,就会占据大量的屏幕空间,甚至装不下而需要手动拖动滚动条,让人眼花缭乱。这时我就想到了利用转义符“\r”,在print完本次的进度之后,下一次直接回车将其清除覆盖,这样就达到了既不占用屏幕又清晰的目的。
大致的方法如下:

1
2
3
4
5
import time #这里是为了用来延时,代替训练的时间
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
for i in range(numOfTimes):
print("\r", "progress percentage:{0}%".format((round(i + 1) * 100 / numOfTimes)), end = "", flush = True)
time.sleep(0.02) #若前面from time import sleep,这里直接sleep(0.02)即可

这里用到了python的format格式化函数,format中计算出的数值对应的位置是{0},将在实际print的过程中被替换。
此外,这里还用到了round()函数,其作用是返回浮点数的四舍五入值。
关于上面在print()函数中出现的flush,文首的参考链接中已给出解释,这里做个搬运:
因为print()函数会把内容放到内存中,内存中的内容并不一定能够及时刷新显示到屏幕中。而当我们使用flush = True之后,会在print结束之后,立即将内存中的东西显示到屏幕上,清空缓存。
基于上述原理,flush大致有下面两个使用场景:

  1. 在循环中,要想每进行一次循环体,在屏幕上更新打印的内容就得使用flush = True的参数设置。(我这里就是这种情况)
  2. 打开一个文件,向其写入字符串,在关闭文件f.close()之前 打开文件是看不到写入的字符的。因此,如果要想在关闭之前实时地看到写入的字符串,那么就应该使用flush = True

利用tqdm库

有需求就有市场,一搜果然还是有库能满足我的需求的。tqdm就是其中之一,它是一个快速,可扩展的python进度条,可以在python长循环中添加一个进度提示信息。
大致用法如下:

1
2
3
4
5
6
import tqdm
import time
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
for i in tqdm.tqdm(range(numOfTimes)):
time.sleep(0.02) #代替训练等耗时过程
pass

也可以直接from tqdm import tqdm,这样后面就不需要tqdm.tqdm了。


利用progressbar

库如其名,这个库就是用来做进度条的。如果没有的话,它和tqdm都可以使用pip来安装。

1
2
3
4
5
6
import progressbar
from time import sleep
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
progress = progressbar.ProgressBar()
for i in progress(range(numOfTimes)):
sleep(0.02)

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>在我们训练模型的时候,我们总希望能够直接看到训练的进度,下面我就总结几个我收集的打印进度的方法。</p><p><strong>Reference + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在我们训练模型的时候,我们总希望能够直接看到训练的进度,下面我就总结几个我收集的打印进度的方法。</p><p><strong>Reference @@ -1359,13 +1359,13 @@ 2020-01-07T14:24:39.000Z 2020-01-19T00:23:09.048Z -

模拟电路实验是我进入大学本科以来第一个付出大量课外时间的实验课,其最后的大项目是让我们自行设计一个电路,而我们小组选择的是有线双工对讲机。今天我就简单对我们小组的设计方案做一个整理。在这里还是非常感谢组员们的共同努力和巨大帮助!

References

电子文献:
http://www.ttic.cc/file/TDA1013B_76329.html.com
https://wenku.baidu.com/view/c8b016e7ed630b1c58eeb520.html
https://tech.hqew.com/fangan_1909806


设计目的

有线对讲机是用导线直接连接进行通话,而双工通信则是像电话机一样同时进行双方的“听”和“讲”。此外,我们希望可以具有音量可调节、消侧音等一些对讲机需要的功能。


设计思路

  1. 利用驻极体话筒将声音信号转化为微弱的电信号。
  2. 通过反相比例放大器将微弱的电信号放大。
  3. 利用相位抵消法实现消侧音。
  4. 只使用一根传输线进行信号互传。
  5. 利用电压跟随器避免远距离导线传输时衰减过大的问题。
  6. 添加了低通滤波器电路,滤去高频的噪音信号。
  7. 使用TDA1013B进行功率放大并将信号传输到扬声器。
  8. 最后由扬声器将电信号转化成声音信号,发出声音。

设计有线双工对讲机的思路可以用如下所示的系统图表示。主要由弱声音采集、前置运算放大器、消侧音电路、减小信号衰减电路、低通滤波器电路、功率放大电路、扬声器等模块组成。


基本原理

  1. 驻极体话筒

    话筒的基本结构由一片单面涂有金属的驻极体薄膜与一个上面有若干小孔的金属电极(背称为背电极)构成。驻极体面与背电极相对,中间有一个极小的空气隙,形成一个以空气隙和驻极体作绝缘介质,以背电极和驻极体上的金属层作为两个电极构成一个平板电容器。电容的两极之间有输出电极。
    由于驻极体薄膜上分布有自由电荷,当声波引起驻极体薄膜振动而产生位移时;改变了电容两极板之间的距离,从而引起电容的容量发生变化,由于驻极体上的电荷数始终保持恒定,根据公式:Q =CU 所以当C变化时必然引起电容器两端电压U的变化,从而输出电信号,实现声电的变换。
    不管是源极输出或漏极输出,驻极体话筒必须提供直流电压才能工作,因为它内部装有场效应管。
  2. 反向比例放大

    相比例放大电路的原理如上图所示,输出信号电压增益为R2与R1之比,相位反相变化180°(后面会再次反相)。在本实验中,我们将两个电阻的比值调整在10~100之间,即放大比例为10~100。
  3. 消侧音

    在模拟音频收发信号共用一个信道的对讲系统中,为减小侧音对通话效果的影响,防止侧音的干扰,所有对讲设备均需增加消侧音电路。一方面让音频发送信号按一定比例出现在传输线上,另一方面让本方音频接收电路获得的信号足够小,不至于说话者从己方喇叭听到自己的声音,提高通话的质量。 在上图所示的串联分压电路中,R1、R2为纯电阻,v1、v2为输入电压,vo为输出电压,据叠加定理:令vo=0,则v1R2+v2R1=0,即:特别地,当R1=R2时,v1=-v2。
    由上式可见,欲使vo=0,v1,v2须满足2个条件:
    1. 每个频率分量的相位相反。
    2. 每个频率分量幅度呈一定比例且比例相同。
      下面是该方法的一种实现方式的电路图: 根据文首所列的参考资料,我们找到了另一种原理相同且成本更低的实现方式,如下图所示。三极管发射极和集电极的信号反相且幅度相同,可以相叠加后将信号消去,一个三极管的作用相当于上述方法中的U1和U2。图中三级管的偏置电路没有画出,C1和C2将直流分量同传输线隔离开。需要特别提出的是,如果可调电阻P1足够大,从而对三极管的偏置影响足够小,可将C2去掉,可调电阻P1直接和三极管的c,e极并联。 需要注意的是,在这里传送到对方的信号的相位再次反相,与原始信号相一致。
      实验中,三极管采用9013,电位器采用104。为了使三极管正常工作,在三极管B端由R1和R3分压,使三极管的静态工作点VCQ≥4.5V,则令VEQ=VCC-VCQ,需要VR2=VR4,因此选R2=R4=1kΩ,则由IEQ=ICQ得VR2=VR4。由于有VBE=0.7V,则VBQ=VEQ+0.7,VBQ+VR1=VCC=12V。此时若ICQ=4mA,经计算,则R1与R3之比约为1.5。我们最终选取R1=6.7kΩ,R3=4.7kΩ来分压。
  4. 减小信号衰减

    由于我们的目标是实现长导线远程传输信号,因此导线上的电阻是不可忽略的,这就导致了信号衰减的问题。在弱电的情况下,解决导线上信号衰减的方法有选取更优质的导线、改用电流信号输出、增大接收端的输入电阻等。
    利用电压跟随器输入电阻高的特性,我们决定在信号接收的两端分别添加一个电压跟随器(如下图所示)来抑制信号传输的衰减。
  5. 低通滤波

    人耳可以听到20HZ到20kHZ的音频信号,而人正常对话所发出的声音频率约为300HZ—3000HZ,频率较低,因此我们设计一个低通语音滤波器来滤除杂音,提高声音清晰度。
    我们的目的是设计一个低频增益A0=2,Q≈1(品质因数,越小则通带或阻带越平坦,电路的稳定性越好), fH=3kHz,图如下所示是一个二阶压控电压源低通滤波器。 根据该电路低频增益A0=K=1+R27/R28=2,可知R27=R28,因此我们选R27=R28=10kΩ。
    根据fC=ωC/2π,当R25=R26=R,C10=C12=C时,有ωC=1/(RC)。
    因此fC=1/2πRC。由所需上限截止频率fH为3kHz,我们选择C=0.01μF,算出R=1/(2πfC) ≈5.3kΩ。这里我们选用4.7kΩ的电阻,可实现近似的功能。
  6. 使用TDA1013B功率放大

    在最后的输出之前,我们需要对信号进行功率的放大。在查阅相关资料之后,我们发现TDA1013B比较符合我们的功能需求。
    TDA1013B是一个音频功率放大器集成电路,内部具有按对数曲线变化的直流音量控制电路,控制范围可达80dB,它具有很宽的电源电压范围(10V~40V),输出功率位4W~10W,是理想的音频功率放大器。
    根据文首列出的数据手册,TDA1013B的伴音电路连接方式如下。 其中各个引脚的功能分别是:
    1脚:电源地。
    2脚:放大器输出,这里作伴音输出。
    3脚:电源。
    4脚:电源。
    5脚:功放输入。
    6脚:控制单元输出。
    7脚:控制电压,这里可用于音量控制。
    8脚:控制单元输入,这里输入音频。
    9脚:信号地。
    通过分析和查阅资料(见本文参考),我们确定了芯片的连接方式如下图所示。

设计实现

如下是我们的仿真总电路图(还缺少最后的两级功率放大电路)。我们使用的是multisim14.0,由于软件的库中没有TDA1013B芯片,且声音信号难以在仿真软件中模拟,因此我们在仿真模拟阶段选择分模块检验功能的实现效果。

我们在第一级放大电路前后使用示波器检测放大效果,我们输入100mV峰峰值、1000Hz频率的交流信号,得到输出如下,其中通道A为放大之后的的信号,通道B显示的是放大之前的信号。

我们还检测了声音低通滤波器的功能实现情况,我们将对应的模块分离出来,利用波特测试仪画出该电路的波特图,结果如下所示。分别使用了对数和线性的横坐标轴(频率),且分别设扫描上限为100kHz和20kHz,由图易知,在3kHz左右处,增益开始下降,基本符合我们的设计要求。

]]>
+

模拟电路实验是我进入大学本科以来第一个付出大量课外时间的实验课,其最后的大项目是让我们自行设计一个电路,而我们小组选择的是有线双工对讲机。今天我就简单对我们小组的设计方案做一个整理。在这里还是非常感谢组员们的共同努力和巨大帮助!

References

电子文献:
http://www.ttic.cc/file/TDA1013B_76329.html.com
https://wenku.baidu.com/view/c8b016e7ed630b1c58eeb520.html
https://tech.hqew.com/fangan_1909806


设计目的

有线对讲机是用导线直接连接进行通话,而双工通信则是像电话机一样同时进行双方的“听”和“讲”。此外,我们希望可以具有音量可调节、消侧音等一些对讲机需要的功能。


设计思路

  1. 利用驻极体话筒将声音信号转化为微弱的电信号。
  2. 通过反相比例放大器将微弱的电信号放大。
  3. 利用相位抵消法实现消侧音。
  4. 只使用一根传输线进行信号互传。
  5. 利用电压跟随器避免远距离导线传输时衰减过大的问题。
  6. 添加了低通滤波器电路,滤去高频的噪音信号。
  7. 使用TDA1013B进行功率放大并将信号传输到扬声器。
  8. 最后由扬声器将电信号转化成声音信号,发出声音。

设计有线双工对讲机的思路可以用如下所示的系统图表示。主要由弱声音采集、前置运算放大器、消侧音电路、减小信号衰减电路、低通滤波器电路、功率放大电路、扬声器等模块组成。


基本原理

  1. 驻极体话筒

    话筒的基本结构由一片单面涂有金属的驻极体薄膜与一个上面有若干小孔的金属电极(背称为背电极)构成。驻极体面与背电极相对,中间有一个极小的空气隙,形成一个以空气隙和驻极体作绝缘介质,以背电极和驻极体上的金属层作为两个电极构成一个平板电容器。电容的两极之间有输出电极。
    由于驻极体薄膜上分布有自由电荷,当声波引起驻极体薄膜振动而产生位移时;改变了电容两极板之间的距离,从而引起电容的容量发生变化,由于驻极体上的电荷数始终保持恒定,根据公式:Q =CU 所以当C变化时必然引起电容器两端电压U的变化,从而输出电信号,实现声电的变换。
    不管是源极输出或漏极输出,驻极体话筒必须提供直流电压才能工作,因为它内部装有场效应管。
  2. 反向比例放大

    相比例放大电路的原理如上图所示,输出信号电压增益为R2与R1之比,相位反相变化180°(后面会再次反相)。在本实验中,我们将两个电阻的比值调整在10~100之间,即放大比例为10~100。
  3. 消侧音

    在模拟音频收发信号共用一个信道的对讲系统中,为减小侧音对通话效果的影响,防止侧音的干扰,所有对讲设备均需增加消侧音电路。一方面让音频发送信号按一定比例出现在传输线上,另一方面让本方音频接收电路获得的信号足够小,不至于说话者从己方喇叭听到自己的声音,提高通话的质量。 在上图所示的串联分压电路中,R1、R2为纯电阻,v1、v2为输入电压,vo为输出电压,据叠加定理:令vo=0,则v1R2+v2R1=0,即:特别地,当R1=R2时,v1=-v2。
    由上式可见,欲使vo=0,v1,v2须满足2个条件:
    1. 每个频率分量的相位相反。
    2. 每个频率分量幅度呈一定比例且比例相同。
      下面是该方法的一种实现方式的电路图: 根据文首所列的参考资料,我们找到了另一种原理相同且成本更低的实现方式,如下图所示。三极管发射极和集电极的信号反相且幅度相同,可以相叠加后将信号消去,一个三极管的作用相当于上述方法中的U1和U2。图中三级管的偏置电路没有画出,C1和C2将直流分量同传输线隔离开。需要特别提出的是,如果可调电阻P1足够大,从而对三极管的偏置影响足够小,可将C2去掉,可调电阻P1直接和三极管的c,e极并联。 需要注意的是,在这里传送到对方的信号的相位再次反相,与原始信号相一致。
      实验中,三极管采用9013,电位器采用104。为了使三极管正常工作,在三极管B端由R1和R3分压,使三极管的静态工作点VCQ≥4.5V,则令VEQ=VCC-VCQ,需要VR2=VR4,因此选R2=R4=1kΩ,则由IEQ=ICQ得VR2=VR4。由于有VBE=0.7V,则VBQ=VEQ+0.7,VBQ+VR1=VCC=12V。此时若ICQ=4mA,经计算,则R1与R3之比约为1.5。我们最终选取R1=6.7kΩ,R3=4.7kΩ来分压。
  4. 减小信号衰减

    由于我们的目标是实现长导线远程传输信号,因此导线上的电阻是不可忽略的,这就导致了信号衰减的问题。在弱电的情况下,解决导线上信号衰减的方法有选取更优质的导线、改用电流信号输出、增大接收端的输入电阻等。
    利用电压跟随器输入电阻高的特性,我们决定在信号接收的两端分别添加一个电压跟随器(如下图所示)来抑制信号传输的衰减。
  5. 低通滤波

    人耳可以听到20HZ到20kHZ的音频信号,而人正常对话所发出的声音频率约为300HZ—3000HZ,频率较低,因此我们设计一个低通语音滤波器来滤除杂音,提高声音清晰度。
    我们的目的是设计一个低频增益A0=2,Q≈1(品质因数,越小则通带或阻带越平坦,电路的稳定性越好), fH=3kHz,图如下所示是一个二阶压控电压源低通滤波器。 根据该电路低频增益A0=K=1+R27/R28=2,可知R27=R28,因此我们选R27=R28=10kΩ。
    根据fC=ωC/2π,当R25=R26=R,C10=C12=C时,有ωC=1/(RC)。
    因此fC=1/2πRC。由所需上限截止频率fH为3kHz,我们选择C=0.01μF,算出R=1/(2πfC) ≈5.3kΩ。这里我们选用4.7kΩ的电阻,可实现近似的功能。
  6. 使用TDA1013B功率放大

    在最后的输出之前,我们需要对信号进行功率的放大。在查阅相关资料之后,我们发现TDA1013B比较符合我们的功能需求。
    TDA1013B是一个音频功率放大器集成电路,内部具有按对数曲线变化的直流音量控制电路,控制范围可达80dB,它具有很宽的电源电压范围(10V~40V),输出功率位4W~10W,是理想的音频功率放大器。
    根据文首列出的数据手册,TDA1013B的伴音电路连接方式如下。 其中各个引脚的功能分别是:
    1脚:电源地。
    2脚:放大器输出,这里作伴音输出。
    3脚:电源。
    4脚:电源。
    5脚:功放输入。
    6脚:控制单元输出。
    7脚:控制电压,这里可用于音量控制。
    8脚:控制单元输入,这里输入音频。
    9脚:信号地。
    通过分析和查阅资料(见本文参考),我们确定了芯片的连接方式如下图所示。

设计实现

如下是我们的仿真总电路图(还缺少最后的两级功率放大电路)。我们使用的是multisim14.0,由于软件的库中没有TDA1013B芯片,且声音信号难以在仿真软件中模拟,因此我们在仿真模拟阶段选择分模块检验功能的实现效果。

我们在第一级放大电路前后使用示波器检测放大效果,我们输入100mV峰峰值、1000Hz频率的交流信号,得到输出如下,其中通道A为放大之后的的信号,通道B显示的是放大之前的信号。

我们还检测了声音低通滤波器的功能实现情况,我们将对应的模块分离出来,利用波特测试仪画出该电路的波特图,结果如下所示。分别使用了对数和线性的横坐标轴(频率),且分别设扫描上限为100kHz和20kHz,由图易知,在3kHz左右处,增益开始下降,基本符合我们的设计要求。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>模拟电路实验是我进入大学本科以来第一个付出大量课外时间的实验课,其最后的大项目是让我们自行设计一个电路,而我们小组选择的是有线双工对讲机。今天我 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>模拟电路实验是我进入大学本科以来第一个付出大量课外时间的实验课,其最后的大项目是让我们自行设计一个电路,而我们小组选择的是有线双工对讲机。今天我 @@ -1385,13 +1385,13 @@ 2020-01-07T13:54:04.000Z 2020-01-19T00:27:07.649Z -

之前每次创建完一个新的文件,文件最上方总会显示一行timescale。当初觉得没什么作用,删了之后依旧没有任何问题便没把它当回事,直到后来做频率计数器的时候我才决定一探究竟。那么这篇文章也就一并谈一下这个问题。

References

电子文献:
https://blog.csdn.net/m0_37652453/article/details/90301902
https://blog.csdn.net/ciscomonkey/article/details/83661395
https://blog.csdn.net/qq_16923717/article/details/81099833


基本原理

一个简易的频率计数器主要由分频器和计数器构成,其基本原理就是记录由分频器得到的一段时间内被测信号上升沿的个数,从而求得被测信号的频率。


控制信号转换模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module control(
output reg Cnt_EN, //enable the counter count, so that the counting period can be controled
output wire Cnt_CR, //clear the counter every time when the measure begins
output wire Latch_Sig, //at its posedge, the value of the counter will be stored/latched
input nRST, //system reset signal
input CP //1Hz standard clock signal
);
always @ (posedge CP or negedge nRST)
begin
if(~nRST) //generate enable counting signal
Cnt_EN = 1'b0; //don't count
else
Cnt_EN = ~Cnt_EN; //two frequency divider for the clock signal
end
assign Latch_Sig = ~Cnt_EN; //generate latch signal
assign Cnt_CR = nRST & (~CP & Latch_Sig); //generate the clear signal for the counter
endmodule

计数模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module counter(
output reg [3:0] Q,
input CR, EN, CP
);
always @ (posedge CP or posedge CR)
begin
if(CR)
Q <= 4'b0000; //reset to zero
else if(~EN)
Q <= Q; //stop counting
else if(Q == 4'b1001)
Q <= 4'b0000;
else
Q <= Q + 1'b1; //counting, plus one
end
endmodule

寄存模块

1
2
3
4
5
6
7
8
9
10
11
12
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module Latch(
output reg [15:0] Qout,
input [15:0] Din,
input Load, CR
);
always @ (posedge Load or posedge CR)
if(CR)
Qout <= 16'h0000; //reset to zero first
else
Qout <= Din;
endmodule

顶层文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module Fre_Measure(
output wire [15:0] BCD, //transfer to display part
input _1HzIN, SigIN, nRST_Key,
output wire Cnt_EN, Cnt_CR, //control signals of the counter
output wire Latch_Sig, //latch singal
output wire [15:0] Cnt //8421BCDcode output
);

//call control block
control U0(Cnt_EN, Cnt_CR, Latch_Sig, nRST_Key, _1HzIN);

//measure counter
counter U1(Cnt[3:0], Cnt_CR, Cnt_EN, SigIN);
counter U2(Cnt[7:4], Cnt_CR, Cnt_EN, ~(Cnt[3:0] == 4'h9));
counter U3(Cnt[11:8], Cnt_CR, Cnt_EN, ~(Cnt[7:0] == 8'h99));
counter U4(Cnt[15:12], Cnt_CR, Cnt_EN, ~(Cnt[11:0] == 12'h999));

//call latch block
Latch U5(BCD, Cnt, Latch_Sig, ~nRST_Key);

endmodule

仿真文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module simulateFile();

wire [15:0] BCD; //transfer to display part
wire [15:0] Cnt; //8421BCDcode output
reg CLK, RST, Signal;
parameter T1 = 0.1, //100Hz
T2 = 0.01, //1000Hz
T3 = 0.002; //5000Hz
wire Cnt_EN, Cnt_CR; //control signals of the counter
wire Latch_Sig; //latch singal

Fre_Measure Watch(BCD, CLK, Signal, RST, Cnt_EN, Cnt_CR, Latch_Sig, Cnt);

initial
begin
RST = 0;
CLK = 0;
Signal = 0;
end

always
forever #10 CLK = ~CLK; //generate clock signal

always #T1 Signal = ~Signal; //T1 or T2 or T3

always
begin
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
end

endmodule

仿真结果

以100Hz(T1)为例,输出的仿真结果如下。

其中CLK是固定的1Hz基准时钟信号;RST是系统需求的复位按键,当按下复位即RST为下降沿时,可以看到计数器Cnt被清零同时第一排译码输出的BCD码也被清零;Signal为输入的信号,这里我使用的是100Hz的,由我自己设定,由于频率较快,可以看到波形图非常密集;Cnt_EN是计数使能信号,可见在它为高电平时,Cnt随着输入信号一样快速计数;Cnt_CR是清零信号,在每次计数使能的上升沿或者复位的下降沿到来时Cnt_CR置零,也就是对Cnt清零操作;此外,当时钟信号到来时,假如系统不在计数(Cnt_EN=0),那么Latch_Sig将置1,也就是把记录数值存入锁存器。
实际上,这种设计方案会存在±1的计数误差,应为输入信号不一定与分频器同周期,即有可能每次测量的起始位置出于输入信号一个周期内的不同状态。


timescale

timescale是Verilog中的预编译指令,指定位于它后边的module的时间单位和时间精度,直到遇到新的timescale指令或者resetall指令。它的语法如下:

1
`timescale time_unit / time_precision

假如我们延时x个time_unit,那延时的总时间time = x * time_unit,但最后真正延时的时间是根据time_precision对time进行四舍五入后的结果。

注意:

  1. time_unit和time_precision只能是1、10和100这三种整数,单位有s、ms、us、ns、ps和fs。
  2. time_precision必须小于等于time_unit。
  3. timescale的时间精度设置是会影响仿真时间的,1ps精度可能是1ns精度仿真时间的一倍还多,并且占用更多的内存,所以如果没有必要,应尽量将时间精度设置得更大一些。

仿真时间

之前进行仿真时,我往往是让它自动运行至系统默认时间然后停止,这就会造成出现好几次重复循环的情况。
在Vivado中,窗口上方有三个类似播放器中的按钮,从左往右依次是:复位、不停运行、按指定时长(在后面的栏中设定)运行。
此外,如果计算不准时间,可以直接在仿真文件末尾或者想要结束的地方使用$stop或者$finifsh来终止仿真。

]]>
+

之前每次创建完一个新的文件,文件最上方总会显示一行timescale。当初觉得没什么作用,删了之后依旧没有任何问题便没把它当回事,直到后来做频率计数器的时候我才决定一探究竟。那么这篇文章也就一并谈一下这个问题。

References

电子文献:
https://blog.csdn.net/m0_37652453/article/details/90301902
https://blog.csdn.net/ciscomonkey/article/details/83661395
https://blog.csdn.net/qq_16923717/article/details/81099833


基本原理

一个简易的频率计数器主要由分频器和计数器构成,其基本原理就是记录由分频器得到的一段时间内被测信号上升沿的个数,从而求得被测信号的频率。


控制信号转换模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module control(
output reg Cnt_EN, //enable the counter count, so that the counting period can be controled
output wire Cnt_CR, //clear the counter every time when the measure begins
output wire Latch_Sig, //at its posedge, the value of the counter will be stored/latched
input nRST, //system reset signal
input CP //1Hz standard clock signal
);
always @ (posedge CP or negedge nRST)
begin
if(~nRST) //generate enable counting signal
Cnt_EN = 1'b0; //don't count
else
Cnt_EN = ~Cnt_EN; //two frequency divider for the clock signal
end
assign Latch_Sig = ~Cnt_EN; //generate latch signal
assign Cnt_CR = nRST & (~CP & Latch_Sig); //generate the clear signal for the counter
endmodule

计数模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module counter(
output reg [3:0] Q,
input CR, EN, CP
);
always @ (posedge CP or posedge CR)
begin
if(CR)
Q <= 4'b0000; //reset to zero
else if(~EN)
Q <= Q; //stop counting
else if(Q == 4'b1001)
Q <= 4'b0000;
else
Q <= Q + 1'b1; //counting, plus one
end
endmodule

寄存模块

1
2
3
4
5
6
7
8
9
10
11
12
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module Latch(
output reg [15:0] Qout,
input [15:0] Din,
input Load, CR
);
always @ (posedge Load or posedge CR)
if(CR)
Qout <= 16'h0000; //reset to zero first
else
Qout <= Din;
endmodule

顶层文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module Fre_Measure(
output wire [15:0] BCD, //transfer to display part
input _1HzIN, SigIN, nRST_Key,
output wire Cnt_EN, Cnt_CR, //control signals of the counter
output wire Latch_Sig, //latch singal
output wire [15:0] Cnt //8421BCDcode output
);

//call control block
control U0(Cnt_EN, Cnt_CR, Latch_Sig, nRST_Key, _1HzIN);

//measure counter
counter U1(Cnt[3:0], Cnt_CR, Cnt_EN, SigIN);
counter U2(Cnt[7:4], Cnt_CR, Cnt_EN, ~(Cnt[3:0] == 4'h9));
counter U3(Cnt[11:8], Cnt_CR, Cnt_EN, ~(Cnt[7:0] == 8'h99));
counter U4(Cnt[15:12], Cnt_CR, Cnt_EN, ~(Cnt[11:0] == 12'h999));

//call latch block
Latch U5(BCD, Cnt, Latch_Sig, ~nRST_Key);

endmodule

仿真文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module simulateFile();

wire [15:0] BCD; //transfer to display part
wire [15:0] Cnt; //8421BCDcode output
reg CLK, RST, Signal;
parameter T1 = 0.1, //100Hz
T2 = 0.01, //1000Hz
T3 = 0.002; //5000Hz
wire Cnt_EN, Cnt_CR; //control signals of the counter
wire Latch_Sig; //latch singal

Fre_Measure Watch(BCD, CLK, Signal, RST, Cnt_EN, Cnt_CR, Latch_Sig, Cnt);

initial
begin
RST = 0;
CLK = 0;
Signal = 0;
end

always
forever #10 CLK = ~CLK; //generate clock signal

always #T1 Signal = ~Signal; //T1 or T2 or T3

always
begin
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
end

endmodule

仿真结果

以100Hz(T1)为例,输出的仿真结果如下。

其中CLK是固定的1Hz基准时钟信号;RST是系统需求的复位按键,当按下复位即RST为下降沿时,可以看到计数器Cnt被清零同时第一排译码输出的BCD码也被清零;Signal为输入的信号,这里我使用的是100Hz的,由我自己设定,由于频率较快,可以看到波形图非常密集;Cnt_EN是计数使能信号,可见在它为高电平时,Cnt随着输入信号一样快速计数;Cnt_CR是清零信号,在每次计数使能的上升沿或者复位的下降沿到来时Cnt_CR置零,也就是对Cnt清零操作;此外,当时钟信号到来时,假如系统不在计数(Cnt_EN=0),那么Latch_Sig将置1,也就是把记录数值存入锁存器。
实际上,这种设计方案会存在±1的计数误差,应为输入信号不一定与分频器同周期,即有可能每次测量的起始位置出于输入信号一个周期内的不同状态。


timescale

timescale是Verilog中的预编译指令,指定位于它后边的module的时间单位和时间精度,直到遇到新的timescale指令或者resetall指令。它的语法如下:

1
`timescale time_unit / time_precision

假如我们延时x个time_unit,那延时的总时间time = x * time_unit,但最后真正延时的时间是根据time_precision对time进行四舍五入后的结果。

注意:

  1. time_unit和time_precision只能是1、10和100这三种整数,单位有s、ms、us、ns、ps和fs。
  2. time_precision必须小于等于time_unit。
  3. timescale的时间精度设置是会影响仿真时间的,1ps精度可能是1ns精度仿真时间的一倍还多,并且占用更多的内存,所以如果没有必要,应尽量将时间精度设置得更大一些。

仿真时间

之前进行仿真时,我往往是让它自动运行至系统默认时间然后停止,这就会造成出现好几次重复循环的情况。
在Vivado中,窗口上方有三个类似播放器中的按钮,从左往右依次是:复位、不停运行、按指定时长(在后面的栏中设定)运行。
此外,如果计算不准时间,可以直接在仿真文件末尾或者想要结束的地方使用$stop或者$finifsh来终止仿真。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>之前每次创建完一个新的文件,文件最上方总会显示一行timescale。当初觉得没什么作用,删了之后依旧没有任何问题便没把它当回事,直到后来做频率 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前每次创建完一个新的文件,文件最上方总会显示一行timescale。当初觉得没什么作用,删了之后依旧没有任何问题便没把它当回事,直到后来做频率 @@ -1400,12 +1400,12 @@ - - + + @@ -1417,13 +1417,13 @@ 2020-01-07T13:11:39.000Z 2020-01-19T00:27:21.473Z -

继上一篇logisim笔记:基本使用及运动码表的实现,我还使用了硬件描述语言对同样需求的运动码表进行了实现,那么就在这里一并也总结一下吧。

References

电子文献:
https://blog.csdn.net/leon_zeng0/article/details/78441871
https://www.cnblogs.com/douzi2/p/5147151.html
https://blog.csdn.net/FPGADesigner/article/details/82425612


整体设计

由于需求与使用Logisim实现时一致,因此我的设计思路也基本沿用上一篇博文中提到的方案。但是要注意的是,这里的000状态并不在作为按键抬起之后的中间态,而是进入系统时的一个默认初始状态。


Vivado中一些高亮的含义

在具体的代码之前,我还想先归纳一下本次实践过程中遇到的和发现的Vivado中一些高亮提醒的含义。

  1. 土黄色高亮

    土黄色高亮出现的原因主要可能是下面三种情况:
    1. 定义重复。
    2. 定义放在了调用处的后面(identifier used before its declaration)。
    3. 声明残缺(empty statement)。
  2. 蓝色高亮

    1. 含有undeclared symbol。
    2. 和上面土黄色高亮相搭配出现,有定义重复时指明重复定义的位置。

16位数值比较器

该模块用于比较两个16位二进制数的大小,以确定是否需要存入记录的数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module _16bit_Comp(
input [15:0] A,
input [15:0] B,
output reg Y
);

always @ (A or B)
begin
if(A < B)
Y <= 1;
else
Y <= 0;
end
endmodule


16位寄存器

TMRecord表示码表暂停时的读数,regRecord表示寄存器中已经存储的记录,初始值为9999。控制信号有使能信号和reset信号。当收到reset信号时,直接将记录改为9999。当有使能信号时,将TMRecord记录下来。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module _16bit_Reg(
input enable,
input [15:0] TMRecord,
input [15:0] regRecord,
input [15:0] maxDefault,
input reset,
input clock,
output reg [15:0] next_record
);

always @ (reset or enable)
begin
if(reset & enable)
next_record <= maxDefault;
else if(enable)
next_record <= TMRecord;
else
next_record <= regRecord;
end
endmodule


数码管显示驱动

将BCD码转化为7位二进制数,即对应7段数码管,用于显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module watchDrive(
input [3:0] BCD,
output reg [6:0] light
);

always @ (BCD)
begin
case(BCD)
0: light = 7'B0111111;
1: light = 7'B0001001;
2: light = 7'B1011110;
3: light = 7'B1011011;
4: light = 7'B1101001;
5: light = 7'B1110011;
6: light = 7'B1110111;
7: light = 7'B0011001;
8: light = 7'B1111111;
9: light = 7'B1111011;
default: light = 7'B0111111;
endcase
end
endmodule


顶层文件

该部分主要包括对各个变量的定义和初始化;状态转换,即共设计了5种状态,对应不同的功能,当按下不同按键时,选择对应的状态并作为次态;数码管的显示与进位,即对数码管4个位置依次改动,从低位开始计算,当进位时产生进位信号到下一位。当有重置信号(这里使用的是reset和start的上升沿)时清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
`timescale 1ns / 1ps
//top module
module runningwatch(
input start,
input stop,
input store,
input reset,
input CLK,
output reg TM_EN, //enable the timer
output reg SD_EN, //enable register
output reg DP_SEL, //the screen show which data
output reg [15:0] regRecord, //data the register storing
output reg [15:0] TMRecord, //timer data
output [6:0] tenmsLight, hunmsLight, onesLight, tensLight
);

reg [3:0] tenMS, hunMS, oneS, tenS; //four 4bit digits from small to high and each from 0 to 9
reg tenmsup, hunmsup, onesup; //the signals if the bigger digit than itself should add

//allocate state
parameter S0 = 3'B000;
//initial state
parameter S1 = 3'B001;
//TIMING:timer set as 00.00,record does not change,show timer,timer counts,TM_EN = 1, SD_EN = 0, DP_SEL = 0
parameter S2 = 3'B010;
//PAUSE:timer does not change,record does not change,show timer,timer does not count,TM_EN = 0, SD_EN = 0, DP_SEL = 0
parameter S3 = 3'B011;
//UPDATE:timer does not change,record set as timer,show register,timer does not count,TM_EN = 0, SD_EN = 1, DP_SEL = 1
parameter S4 = 3'B100;
//KEEP:timer does not change,record does not change,show register,timer does not count,TM_EN = 0, SD_EN = 0, DP_SEL = 1
parameter S5 = 3'B101;
//RESET:timer set as 00.00,record set as 99.99,show timer,timer does not count,TM_EN = 0, SD_EN = 1, DP_SEL = 0

reg [2:0] state;
reg [2:0] next_state;

//reg CLK;
initial //only run once
begin
regRecord = 16'B0010011100001111;
TMRecord = 16'B0000000000000000;

state = S0;

tenMS = 0; hunMS = 0; oneS = 0; tenS = 0;
end

//to judge if store the timer's data
wire new;
reg newRecord;
_16bit_Comp comparator(TMRecord, regRecord, new); //compare
always @ (new)
newRecord = new;

reg [15:0] MAX = 16'B0010011100001111;

//sequential logic part
always @ (posedge CLK) //update the state at each posedge
begin
state <= next_state;
end

//combinatory logic part
//state transform
always @ (state or start or stop or store or reset or newRecord)
begin
next_state = S0; //if not press the key, back to the initial state

case(state)

S0: //initial state
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S1: //TIMING
begin
TM_EN = 1; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S2: //PAUSE
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S3: //UPDATE
begin
TM_EN = 0; SD_EN = 1; DP_SEL = 1;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S4: //KEEP
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 1;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S5: //RESET
begin
TM_EN = 0; SD_EN = 1; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

default: begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end //default initial state
endcase
end

//reset to zero
always @ (posedge reset or posedge start)
begin
tenMS <= 0; hunMS <= 0; oneS <= 0; tenS <= 0;
TMRecord <= 0;
end

//the followings are which have stated before:
//reg [3:0] tenMS, hunMS, oneS, tenS are four 4bit digits from small to high and each from 0 to 9
//reg tenmsup, hunmsup, onesup are the signals if the bigger digit than itself should add
//timer, divide into four digits
//10ms
always @ (posedge CLK)
begin
if(TM_EN)
begin
TMRecord = TMRecord + 1;
if(tenMS < 9)
begin tenMS <= tenMS + 1; tenmsup <= 0; end
else
begin tenMS <= 0; tenmsup <= 1; end
end
end
//100ms
always @ (posedge tenmsup)
begin
if(TM_EN)
begin
if(hunMS < 9)
begin hunMS <= hunMS + 1; hunmsup <= 0; end
else
begin hunMS <= 0; hunmsup <= 1; end
end
end

//1s
always @ (posedge hunmsup)
begin
if(TM_EN)
begin
if(oneS < 9)
begin oneS <= oneS + 1; onesup <= 0; end
else
begin oneS <= 0; onesup <= 1; end
end
end

//10s
always @ (posedge onesup)
begin
if(TM_EN)
begin
if(tenS < 9)
tenS <= oneS + 1;
else
oneS <= 0;
end
end

//save to the register
wire [15:0] newReg;
_16bit_Reg register(SD_EN, TMRecord, regRecord, MAX, reset, CLK, newReg);
always @ (newReg)
regRecord = newReg;

//change BCD to tube lights
watchDrive TENms(tenMS, tenmsLight);
watchDrive HUNms(hunMS, hunmsLight);
watchDrive ONEs(oneS, onesLight);
watchDrive TENs(tenS, tensLight);

endmodule


仿真文件

最后我们还需要自行编写一个仿真文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
module simulateFile();
reg start, stop, store, reset;
wire TM_EN, SD_EN, DP_SEL;
wire [15:0] regRecord;
wire [15:0] TMRecord;
wire [6:0] tenmsLight;
wire [6:0] hunmsLight;
wire [6:0] onesLight;
wire [6:0] tensLight;
reg CLK;

runningwatch watch(start, stop, store, reset, CLK,
TM_EN, SD_EN, DP_SEL, regRecord, TMRecord,
tenmsLight, hunmsLight, onesLight, tensLight);
initial
begin
CLK = 0;
start = 0;
stop = 0;
store = 0;
reset = 0;
end

begin
always
//generate clock signal
forever #10 CLK = ~CLK;

//always #100
//{start, stop, store, reset} = 4'B0000;
//begin start = 0; stop = 0; store = 0; reset = 0; end
always
begin
#50
{start, stop, store, reset} = 4'B0001;
#50
{start, stop, store, reset} = 4'B1000;
#500
{start, stop, store, reset} = 4'B0100;
#50
{start, stop, store, reset} = 4'B0010;
#50
{start, stop, store, reset} = 4'B1000;
#50
{start, stop, store, reset} = 4'B0100;
#100
{start, stop, store, reset} = 4'B0010;
#50
{start, stop, store, reset} = 4'B0001;
#50
$stop; //stop simulation
end
end
endmodule


仿真结果

Run simulation,得到如下输出波形图。


遇到的问题

  1. 报错:[Synth 8-462] no clock signal specified in event control

    我原本直接在顶层文件中定义时钟并使用forever生成连续的时钟信号,结果出现了如上报错。
    在仿佛调整后,最后发现解决的办法是将时钟信号放在仿真文件里生成然后作为input输入到顶层文件。
  2. 使用模块的输出赋值时遇到问题

    这个问题的主要原因还是因为我对于verilog赋值规则以及变量性质的不熟悉,这里做一个小归纳:
    1. 给wire赋值必须用assign。
    2. 给reg赋值用always。
    3. 使用非阻塞赋值时,reg不能给wire赋值,反之则可以。
    4. 使用阻塞赋值时,reg可以给wire赋值,反之则不行。
]]>
+

继上一篇logisim笔记:基本使用及运动码表的实现,我还使用了硬件描述语言对同样需求的运动码表进行了实现,那么就在这里一并也总结一下吧。

References

电子文献:
https://blog.csdn.net/leon_zeng0/article/details/78441871
https://www.cnblogs.com/douzi2/p/5147151.html
https://blog.csdn.net/FPGADesigner/article/details/82425612


整体设计

由于需求与使用Logisim实现时一致,因此我的设计思路也基本沿用上一篇博文中提到的方案。但是要注意的是,这里的000状态并不在作为按键抬起之后的中间态,而是进入系统时的一个默认初始状态。


Vivado中一些高亮的含义

在具体的代码之前,我还想先归纳一下本次实践过程中遇到的和发现的Vivado中一些高亮提醒的含义。

  1. 土黄色高亮

    土黄色高亮出现的原因主要可能是下面三种情况:
    1. 定义重复。
    2. 定义放在了调用处的后面(identifier used before its declaration)。
    3. 声明残缺(empty statement)。
  2. 蓝色高亮

    1. 含有undeclared symbol。
    2. 和上面土黄色高亮相搭配出现,有定义重复时指明重复定义的位置。

16位数值比较器

该模块用于比较两个16位二进制数的大小,以确定是否需要存入记录的数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module _16bit_Comp(
input [15:0] A,
input [15:0] B,
output reg Y
);

always @ (A or B)
begin
if(A < B)
Y <= 1;
else
Y <= 0;
end
endmodule


16位寄存器

TMRecord表示码表暂停时的读数,regRecord表示寄存器中已经存储的记录,初始值为9999。控制信号有使能信号和reset信号。当收到reset信号时,直接将记录改为9999。当有使能信号时,将TMRecord记录下来。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module _16bit_Reg(
input enable,
input [15:0] TMRecord,
input [15:0] regRecord,
input [15:0] maxDefault,
input reset,
input clock,
output reg [15:0] next_record
);

always @ (reset or enable)
begin
if(reset & enable)
next_record <= maxDefault;
else if(enable)
next_record <= TMRecord;
else
next_record <= regRecord;
end
endmodule


数码管显示驱动

将BCD码转化为7位二进制数,即对应7段数码管,用于显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module watchDrive(
input [3:0] BCD,
output reg [6:0] light
);

always @ (BCD)
begin
case(BCD)
0: light = 7'B0111111;
1: light = 7'B0001001;
2: light = 7'B1011110;
3: light = 7'B1011011;
4: light = 7'B1101001;
5: light = 7'B1110011;
6: light = 7'B1110111;
7: light = 7'B0011001;
8: light = 7'B1111111;
9: light = 7'B1111011;
default: light = 7'B0111111;
endcase
end
endmodule


顶层文件

该部分主要包括对各个变量的定义和初始化;状态转换,即共设计了5种状态,对应不同的功能,当按下不同按键时,选择对应的状态并作为次态;数码管的显示与进位,即对数码管4个位置依次改动,从低位开始计算,当进位时产生进位信号到下一位。当有重置信号(这里使用的是reset和start的上升沿)时清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
`timescale 1ns / 1ps
//top module
module runningwatch(
input start,
input stop,
input store,
input reset,
input CLK,
output reg TM_EN, //enable the timer
output reg SD_EN, //enable register
output reg DP_SEL, //the screen show which data
output reg [15:0] regRecord, //data the register storing
output reg [15:0] TMRecord, //timer data
output [6:0] tenmsLight, hunmsLight, onesLight, tensLight
);

reg [3:0] tenMS, hunMS, oneS, tenS; //four 4bit digits from small to high and each from 0 to 9
reg tenmsup, hunmsup, onesup; //the signals if the bigger digit than itself should add

//allocate state
parameter S0 = 3'B000;
//initial state
parameter S1 = 3'B001;
//TIMING:timer set as 00.00,record does not change,show timer,timer counts,TM_EN = 1, SD_EN = 0, DP_SEL = 0
parameter S2 = 3'B010;
//PAUSE:timer does not change,record does not change,show timer,timer does not count,TM_EN = 0, SD_EN = 0, DP_SEL = 0
parameter S3 = 3'B011;
//UPDATE:timer does not change,record set as timer,show register,timer does not count,TM_EN = 0, SD_EN = 1, DP_SEL = 1
parameter S4 = 3'B100;
//KEEP:timer does not change,record does not change,show register,timer does not count,TM_EN = 0, SD_EN = 0, DP_SEL = 1
parameter S5 = 3'B101;
//RESET:timer set as 00.00,record set as 99.99,show timer,timer does not count,TM_EN = 0, SD_EN = 1, DP_SEL = 0

reg [2:0] state;
reg [2:0] next_state;

//reg CLK;
initial //only run once
begin
regRecord = 16'B0010011100001111;
TMRecord = 16'B0000000000000000;

state = S0;

tenMS = 0; hunMS = 0; oneS = 0; tenS = 0;
end

//to judge if store the timer's data
wire new;
reg newRecord;
_16bit_Comp comparator(TMRecord, regRecord, new); //compare
always @ (new)
newRecord = new;

reg [15:0] MAX = 16'B0010011100001111;

//sequential logic part
always @ (posedge CLK) //update the state at each posedge
begin
state <= next_state;
end

//combinatory logic part
//state transform
always @ (state or start or stop or store or reset or newRecord)
begin
next_state = S0; //if not press the key, back to the initial state

case(state)

S0: //initial state
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S1: //TIMING
begin
TM_EN = 1; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S2: //PAUSE
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S3: //UPDATE
begin
TM_EN = 0; SD_EN = 1; DP_SEL = 1;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S4: //KEEP
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 1;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S5: //RESET
begin
TM_EN = 0; SD_EN = 1; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

default: begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end //default initial state
endcase
end

//reset to zero
always @ (posedge reset or posedge start)
begin
tenMS <= 0; hunMS <= 0; oneS <= 0; tenS <= 0;
TMRecord <= 0;
end

//the followings are which have stated before:
//reg [3:0] tenMS, hunMS, oneS, tenS are four 4bit digits from small to high and each from 0 to 9
//reg tenmsup, hunmsup, onesup are the signals if the bigger digit than itself should add
//timer, divide into four digits
//10ms
always @ (posedge CLK)
begin
if(TM_EN)
begin
TMRecord = TMRecord + 1;
if(tenMS < 9)
begin tenMS <= tenMS + 1; tenmsup <= 0; end
else
begin tenMS <= 0; tenmsup <= 1; end
end
end
//100ms
always @ (posedge tenmsup)
begin
if(TM_EN)
begin
if(hunMS < 9)
begin hunMS <= hunMS + 1; hunmsup <= 0; end
else
begin hunMS <= 0; hunmsup <= 1; end
end
end

//1s
always @ (posedge hunmsup)
begin
if(TM_EN)
begin
if(oneS < 9)
begin oneS <= oneS + 1; onesup <= 0; end
else
begin oneS <= 0; onesup <= 1; end
end
end

//10s
always @ (posedge onesup)
begin
if(TM_EN)
begin
if(tenS < 9)
tenS <= oneS + 1;
else
oneS <= 0;
end
end

//save to the register
wire [15:0] newReg;
_16bit_Reg register(SD_EN, TMRecord, regRecord, MAX, reset, CLK, newReg);
always @ (newReg)
regRecord = newReg;

//change BCD to tube lights
watchDrive TENms(tenMS, tenmsLight);
watchDrive HUNms(hunMS, hunmsLight);
watchDrive ONEs(oneS, onesLight);
watchDrive TENs(tenS, tensLight);

endmodule


仿真文件

最后我们还需要自行编写一个仿真文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
module simulateFile();
reg start, stop, store, reset;
wire TM_EN, SD_EN, DP_SEL;
wire [15:0] regRecord;
wire [15:0] TMRecord;
wire [6:0] tenmsLight;
wire [6:0] hunmsLight;
wire [6:0] onesLight;
wire [6:0] tensLight;
reg CLK;

runningwatch watch(start, stop, store, reset, CLK,
TM_EN, SD_EN, DP_SEL, regRecord, TMRecord,
tenmsLight, hunmsLight, onesLight, tensLight);
initial
begin
CLK = 0;
start = 0;
stop = 0;
store = 0;
reset = 0;
end

begin
always
//generate clock signal
forever #10 CLK = ~CLK;

//always #100
//{start, stop, store, reset} = 4'B0000;
//begin start = 0; stop = 0; store = 0; reset = 0; end
always
begin
#50
{start, stop, store, reset} = 4'B0001;
#50
{start, stop, store, reset} = 4'B1000;
#500
{start, stop, store, reset} = 4'B0100;
#50
{start, stop, store, reset} = 4'B0010;
#50
{start, stop, store, reset} = 4'B1000;
#50
{start, stop, store, reset} = 4'B0100;
#100
{start, stop, store, reset} = 4'B0010;
#50
{start, stop, store, reset} = 4'B0001;
#50
$stop; //stop simulation
end
end
endmodule


仿真结果

Run simulation,得到如下输出波形图。


遇到的问题

  1. 报错:[Synth 8-462] no clock signal specified in event control

    我原本直接在顶层文件中定义时钟并使用forever生成连续的时钟信号,结果出现了如上报错。
    在仿佛调整后,最后发现解决的办法是将时钟信号放在仿真文件里生成然后作为input输入到顶层文件。
  2. 使用模块的输出赋值时遇到问题

    这个问题的主要原因还是因为我对于verilog赋值规则以及变量性质的不熟悉,这里做一个小归纳:
    1. 给wire赋值必须用assign。
    2. 给reg赋值用always。
    3. 使用非阻塞赋值时,reg不能给wire赋值,反之则可以。
    4. 使用阻塞赋值时,reg可以给wire赋值,反之则不行。
]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>继上一篇<a href="https://gsy00517.github.io/logisim20200107121101/" target=" + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>继上一篇<a href="https://gsy00517.github.io/logisim20200107121101/" target=" @@ -1434,12 +1434,12 @@ - - + + @@ -1449,15 +1449,15 @@ https://gsy00517.github.io/logisim20200107121101/ 2020-01-07T04:11:01.000Z - 2020-01-07T14:29:31.608Z + 2020-05-03T01:38:22.464Z -

似乎好久没有写新的博文了。由于今年的春节较往年要早,近一个月来,各门考试接踵而至让我忙得不可开交。虽然非常赶,终于还是考完了。当初想都不敢想的13天考10门的考试周也就这样熬过去了。事实证明,即便是很大的困难,逼一下自己还是能挺过来的。不过最后还是和往年的冬天一样发了个一年一度的高烧,真的难受,以后还得更加注重自己的身体。
废话不多说,这几天就打算先把这学期期末阶段一些有意义、有价值的的知识与经历整理记录一下。首先是一个运动码表的大作业,本文主要是使用Logisim对运动码表进行实现的方法与过程。


需求分析

该项目要求我们设计一个简单的运动码表,包括四个按键组成的输入模块和四个7段数码管组成的输出模块。四个按键分别是:Start,按下时计时器归零并重新开始计时;Stop,按下时停止计时并显示计时数据;Store,按下时尝试更新系统记录,要求记录当前码表值,若已有记录,则和当前待记录的值作对比,如果计时数据比记录数据要小即用时要短,则更新系统记录并显示;Reset,按下时进行复位,将计时数据置为00.00,系统记录置为99.99。


Logisim使用

在具体说明实现的方法之前,我想先把之前整理的一些Logisim的基本使用做一个简短的总结。Logisim的使用其实不难,可以参考网上整理的Logisim文档翻译就可以快速入门。实际上,在具备一定数字电路知识的情况下,到处点点也能够学会Logisim的基本使用方法。
Logisim为数字电路的设计提供了很大的帮助,我们可以通过填写真值表或者输入逻辑函数快速生成原本手动连线根本无法完成的复杂电路。
如果想要获得Logisim最新的优化版本,可以加入华科谭志虎教授创建的计算机硬件系统设计交流群(群号:957283876),此群汇集了多个高校的学生,是一个比较活跃的技术交流群,谭教授秒回且超赞的解答真的忍不住让人点赞!下面是一些我当初刚使用时记录在note上的一些Logisim的使用与常见处理,分享在此。

  1. 引脚

    在Logisim中,引脚即Pin,可以使用键盘上、下、左、右光标键来改变引脚的朝向。

    注意:这里该表朝向不是按照旋转方向来的,而是按哪个方向就是指向哪个方向。

    此外,根据形状的不同,引脚分为输入引脚(较方)和输出(较圆)引脚,可以使用UI左上角最左侧的手型的戳工具点击对应的引脚来修改引脚的值(高电平、低电平、未知x态)。
    当我们选中引脚时,按下alt+数字组合,就可以改变到对应的位宽。

  2. 与门

    选中与门,按下数字键,就修改输入引脚个数。
  3. 线颜色的含义

    • 亮绿色:高电平。
    • 深绿色:低电平。
    • 蓝色:未知状态。
    • 灰色:飞线。
    • 红色:信号冲突。
    • 黑色:多位总线。
    • 橙色:位宽不匹配。
  4. 时钟

    • ctrl+K:驱动与关闭时钟连续运行。
    • ctrl+T:驱动时钟单步运行。
  5. 其它快捷键

    上面列出的都是一些并比较典型的特例,别的地方使用基本类似,可以多多尝试。下面再列出两个比较常用的快捷键:
    • ctrl+D:创建副本。
    • ctrl+Z:撤销。

整体设计

为了实现运动码表的功能,我们将整个项目拆分成如下几个模块:

  • 计时模块(包括计时使能模块)。
  • 码表驱动与显示模块。
  • 系统记录数据寄存模块。
  • 码表状态功能控制模块。
  • 数值比较模块。
  • 数值选择模块。

此外,根据需求分析,我们设计了如下6个状态,并构建状态机如下:

  • 000 中间态:每次按键弹起后,回到该状态。
  • 101 复位:计时变成00.00,记录变成99.99,显示计时数值,时钟不计时。
  • 001 计时:计时变成00.00,记录不变,显示计时数值,时钟计时。
  • 010 停止:计时不变,记录不变,显示计时数值,时钟不计时。
  • 011 更新(小于系统记录NewRecord=1):计时不变,记录变成计时,显示记录数值,时钟不计时。
  • 100 不更新(大于等于系统记录NewRecord=0):计时不变,记录不变,显示记录数值,时钟不计时。

可得状态转换关系的真值表如下所示:

为了实现上述状态转换与控制信号输出,我们设计了如下码表控制电路。

其中SD-EN控制寄存器使能,DP-SEL控制码表显示数值的选择,TM-Reset控制是否复位。


设计实现

这是设计完成后最终circ文件中所包含的内容,下面我简述一下各个模块中的内容及思路。
数码管驱动:即将4位2进制数转化成7个二进制信号,驱动7段数码管的亮灭。
4位BCD计数器:基于下面的BCD计数器状态转换(即在0-9之间递增循环)和BCD计数器输出函数(即在达到9时输出进位信号)。
码表计数器:由四个4位BCD计数器组成。
码表显示驱动:即将四个4位二进制组成的数字通过四个7段数码管显示出来,内部基于上面的显示驱动转换电路即数码管驱动的封装。
码表控制器:上文的设计中已经提及,基于码表控制器状态转换(输入信号+现态->次态)和码表控制器输出函数(现态->控制信号)。
计时使能:这里我比较巧妙地运用了D触发器的置位清零两个端,将在后文提及。
最后就是运动码表的总电路图:


遇到的问题

  1. 复位后自动开始计时

    这是最让我头疼的一个问题,由于上述状态机的设计方法和Logisim中按键在鼠标松开后自动弹起的功能,导致在我按下Reset清零后系统会自动重新开始计时(因为这时的现态已经不是复位状态),这显然是不符合需求的。为了使计时使能在下一次按键到来之前能保持现态,我将复位控制信号从状态转换电路中单独取出,并使用一个D触发器的置位清零两个端子实现了这个保持功能,效果不错。
  2. 计时器遇到9时跳跃进位

    当最初的电路实现完成后,我兴奋地开始计时,结果计时器的花式进位方式顿时让我傻了眼。仔细研究后我发现,我起初设计的电路在进位时只考虑了低一位计数器传来的进位信号,而事实上,高位的进位条件往往是由后几位共同决定的,为此,修改电路如下: 问题解决。
  3. 码表在更新数据的前一个时钟内会显示较大的记录数值

    这个问题其实不是非常重要,但同班的一位同学还是注意到了这点。也就是说,当计时数据比系统记录要小的时候,系统应该更新记录并显示最好的成绩,然而当按下Store进行存储更新时,在前一个时钟周期内,码表会短暂地先显示原本较大即较差地系统记录,这是不希望出现的。
    解决的办法可以在寄存器和显示选择器之间添加一个三态门,具体可以看上文中的总电路图。
]]>
+

似乎好久没有写新的博文了。由于今年的春节较往年要早,近一个月来,各门考试接踵而至让我忙得不可开交。虽然非常赶,终于还是考完了。当初想都不敢想的13天考10门的考试周也就这样熬过去了。事实证明,即便是很大的困难,逼一下自己还是能挺过来的。不过最后还是和往年的冬天一样发了个一年一度的高烧,真的难受,以后还得更加注重自己的身体。
废话不多说,这几天就打算先把这学期期末阶段一些有意义、有价值的的知识与经历整理记录一下。首先是一个运动码表的大作业,本文主要是使用Logisim对运动码表进行实现的方法与过程。


需求分析

该项目要求我们设计一个简单的运动码表,包括四个按键组成的输入模块和四个7段数码管组成的输出模块。四个按键分别是:Start,按下时计时器归零并重新开始计时;Stop,按下时停止计时并显示计时数据;Store,按下时尝试更新系统记录,要求记录当前码表值,若已有记录,则和当前待记录的值作对比,如果计时数据比记录数据要小即用时要短,则更新系统记录并显示;Reset,按下时进行复位,将计时数据置为00.00,系统记录置为99.99。


Logisim使用

在具体说明实现的方法之前,我想先把之前整理的一些Logisim的基本使用做一个简短的总结。Logisim的使用其实不难,可以参考网上整理的Logisim文档翻译就可以快速入门。实际上,在具备一定数字电路知识的情况下,到处点点也能够学会Logisim的基本使用方法。
Logisim为数字电路的设计提供了很大的帮助,我们可以通过填写真值表或者输入逻辑函数快速生成原本手动连线根本无法完成的复杂电路。
如果想要获得Logisim最新的优化版本,可以加入华科谭志虎教授创建的计算机硬件系统设计交流群(群号:957283876),此群汇集了多个高校的学生,是一个比较活跃的技术交流群,谭教授秒回且超赞的解答真的忍不住让人点赞!下面是一些我当初刚使用时记录在note上的一些Logisim的使用与常见处理,分享在此。

  1. 引脚

    在Logisim中,引脚即Pin,可以使用键盘上、下、左、右光标键来改变引脚的朝向。

    注意:这里该表朝向不是按照旋转方向来的,而是按哪个方向就是指向哪个方向。

    此外,根据形状的不同,引脚分为输入引脚(较方)和输出(较圆)引脚,可以使用UI左上角最左侧的手型的戳工具点击对应的引脚来修改引脚的值(高电平、低电平、未知x态)。
    当我们选中引脚时,按下alt+数字组合,就可以改变到对应的位宽。

  2. 与门

    选中与门,按下数字键,就修改输入引脚个数。
  3. 线颜色的含义

    • 亮绿色:高电平。
    • 深绿色:低电平。
    • 蓝色:未知状态。
    • 灰色:飞线。
    • 红色:信号冲突。
    • 黑色:多位总线。
    • 橙色:位宽不匹配。
  4. 时钟

    • ctrl+K:驱动与关闭时钟连续运行。
    • ctrl+T:驱动时钟单步运行。
  5. 其它快捷键

    上面列出的都是一些并比较典型的特例,别的地方使用基本类似,可以多多尝试。下面再列出两个比较常用的快捷键:
    • ctrl+D:创建副本。
    • ctrl+Z:撤销。

整体设计

为了实现运动码表的功能,我们将整个项目拆分成如下几个模块:

  • 计时模块(包括计时使能模块)。
  • 码表驱动与显示模块。
  • 系统记录数据寄存模块。
  • 码表状态功能控制模块。
  • 数值比较模块。
  • 数值选择模块。

此外,根据需求分析,我们设计了如下6个状态,并构建状态机如下:

  • 000 中间态:每次按键弹起后,回到该状态。
  • 101 复位:计时变成00.00,记录变成99.99,显示计时数值,时钟不计时。
  • 001 计时:计时变成00.00,记录不变,显示计时数值,时钟计时。
  • 010 停止:计时不变,记录不变,显示计时数值,时钟不计时。
  • 011 更新(小于系统记录NewRecord=1):计时不变,记录变成计时,显示记录数值,时钟不计时。
  • 100 不更新(大于等于系统记录NewRecord=0):计时不变,记录不变,显示记录数值,时钟不计时。

可得状态转换关系的真值表如下所示:

为了实现上述状态转换与控制信号输出,我们设计了如下码表控制电路。

其中SD-EN控制寄存器使能,DP-SEL控制码表显示数值的选择,TM-Reset控制是否复位。


设计实现

这是设计完成后最终circ文件中所包含的内容,下面我简述一下各个模块中的内容及思路。
数码管驱动:即将4位2进制数转化成7个二进制信号,驱动7段数码管的亮灭。
4位BCD计数器:基于下面的BCD计数器状态转换(即在0-9之间递增循环)和BCD计数器输出函数(即在达到9时输出进位信号)。
码表计数器:由四个4位BCD计数器组成。
码表显示驱动:即将四个4位二进制组成的数字通过四个7段数码管显示出来,内部基于上面的显示驱动转换电路即数码管驱动的封装。
码表控制器:上文的设计中已经提及,基于码表控制器状态转换(输入信号+现态->次态)和码表控制器输出函数(现态->控制信号)。
计时使能:这里我比较巧妙地运用了D触发器的置位清零两个端,将在后文提及。
最后就是运动码表的总电路图:


遇到的问题

  1. 复位后自动开始计时

    这是最让我头疼的一个问题,由于上述状态机的设计方法和Logisim中按键在鼠标松开后自动弹起的功能,导致在我按下Reset清零后系统会自动重新开始计时(因为这时的现态已经不是复位状态),这显然是不符合需求的。为了使计时使能在下一次按键到来之前能保持现态,我将复位控制信号从状态转换电路中单独取出,并使用一个D触发器的置位清零两个端子实现了这个保持功能,效果不错。
  2. 计时器遇到9时跳跃进位

    当最初的电路实现完成后,我兴奋地开始计时,结果计时器的花式进位方式顿时让我傻了眼。仔细研究后我发现,我起初设计的电路在进位时只考虑了低一位计数器传来的进位信号,而事实上,高位的进位条件往往是由后几位共同决定的,为此,修改电路如下: 问题解决。
  3. 码表在更新数据的前一个时钟内会显示较大的记录数值

    这个问题其实不是非常重要,但同班的一位同学还是注意到了这点。也就是说,当计时数据比系统记录要小的时候,系统应该更新记录并显示最好的成绩,然而当按下Store进行存储更新时,在前一个时钟周期内,码表会短暂地先显示原本较大即较差地系统记录,这是不希望出现的。
    解决的办法可以在寄存器和显示选择器之间添加一个三态门,具体可以看上文中的总电路图。

实现

看到评论区有许多同学似乎有一些困难无法解决,因此我在此提供了我最后实现的百度网盘链接,提取码为e8i1

注意:文件中包含logisim和运动码表的电路文件。我在当初实现时,一些模块是直接使用了库中已有的器件,并没有全部从最基础的器件开始实现。此外我的思路和华科谭志虎教授的慕课在细节上也有稍许不同,一些要点都已写在本篇文章中了。本人的实现仅供参考,如是学校作业,还需根据具体要求进行调整或者修改。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>似乎好久没有写新的博文了。由于今年的春节较往年要早,近一个月来,各门考试接踵而至让我忙得不可开交。虽然非常赶,终于还是考完了。当初想都不敢想的1 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>似乎好久没有写新的博文了。由于今年的春节较往年要早,近一个月来,各门考试接踵而至让我忙得不可开交。虽然非常赶,终于还是考完了。当初想都不敢想的1 @@ -1468,10 +1468,10 @@ - - + + @@ -1481,13 +1481,13 @@ 2019-11-15T16:35:45.000Z 2020-02-07T09:31:14.688Z -

上周科学计算引论结课了,借着写上机报告的机会,我把书本上所有的求解非线性方程的方法(标量型)都用matlab实现了一下,并对一个实际工程问题进行求解。关于实现过程中遇到的问题以及注意事项,我已写在matlab笔记:久未使用之后踩的一堆坑内。


实际问题

在电机学中,凸极同步发电机的功角特性可表示为:

式中,$P_{em}$表示发电机的电磁功率;$E_{0}$表示发电机电势;$V$表示发电机端电压;$x_{q}$表示横轴同步电抗;$x_{d}$表示纵轴同步电抗;$\theta$表示功率角,$\theta \in \left ( 0,\frac{\pi }{2} \right )$。
如令$\frac{E_{0}V}{x_{d}}=P_{j}$,$V^{2}\left ( \frac{1}{x_{q}}-\frac{1}{x_{d}} \right )=P_{2e}$,则上式可以简化为:

在电力系统稳定计算中,我们往往要由上式求出功率角$\theta$。我们可以使用几何方法求解,也可以利用迭代法求解该非线性方程。
我们将上式变为:

以许实章编《电机学习题集》第367页26-1为例,将$P_{em}=1$,$P_{j}=1.878$,$P_{2e}=0.75$代入,得到方程:


问题求解

在《计算方法》第二章,我们学习了一些非线性方程的数值解法,这里我们分别使用几何法和迭代法求解上述问题并进行比较。设方程求解的预定精度为$0.001^{\circ}$即$1.7\times 10^{-3}rad$,由闭区间上连续函数的性质和初步估计,可确定方程的解位于区间$[0,\frac{\pi }{6}]$。

几何方法

由几何法的求解方法,我们定义函数:

求解$\theta$即求解$f(\theta )$在$[0,\frac{\pi }{6}]$上的零点。
在matlab中,将该函数实现如下:

1
2
3
4
5
6
function [y] = func(x)
%几何法函数方程
%统一使用弧度制
format long %为提高精度,保留更多位数
y = asin(1 / (1.878 + 0.75 * cos(x))) - x;
end

由于matlab计算过程中默认保留$4$位小数,为了提高精度,我使用了format long保留更多有效数字。值得注意的是,计算时统一使用弧度制,待求出解之后再使用rad2deg函数转化为角度。

二分法

根据二分法的格式,编写二分法函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function [errors, ans, time] = bisection(low, high, max, stopError) 
%二分法
%func是待求零点的函数
%low,high分别是解区间的上下限
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
fl = func(low);
fh = func(high);
error = high - low;
for i = 1 : max
mid = (low + high ) / 2;
fm = func(mid);
if fm * fl > 0
low = mid;
else high = mid;
end
error = high - low;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(mid); %转换成角度
end

输入命令[errorsB, ans, time] = bisection(0, pi / 6, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
errorsB =

列 1 至 5

0.261799387799149 0.130899693899575 0.065449846949787 0.032724923474894 0.016362461737447

列 6 至 10

0.008181230868723 0.004090615434362 0.002045307717181 0.001022653858590 0.000511326929295

列 11 至 15

0.000255663464648 0.000127831732324 0.000063915866162 0.000031957933081 0.000015978966540


ans =

22.909240722656250


time =

15

使用二分法共迭代$15$次,求得结果为$22.909240722656250^{\circ}$。此外,迭代误差为$0.000015978966540$,符合预设精度要求。然而,此种方法计算次数较多,因此我又尝试了下面的方法。

弦截法

根据弦截法的计算方法,编写弦截法函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function [errors, ans, time] = linecut(a, b, max, stopError)
%弦截法
%func是待求零点的函数
%a,b是在解的领域取的两点,这里取解区间的两个端点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
fa = func(a);
fb = func(b);
error = abs(a - b);
for i = 1 : max
x = b - (b - a) * fb / (fb - fa);
a = b;
b = x;
fa = func(a);
fb = func(b);
error = abs(a - b);
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(b); %转换成角度
end

输入命令[errorsL, ans, time] = linecut(0, pi / 6, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsL =

0.120609794441825 0.003164447033051 0.000026217912123 0.000000005427336


ans =

22.909760215805360


time =

4

与上面的二分法相比,弦截法只需计算$4$次,效率大大提高。计算所得结果为$22.909760215805360^{\circ}$,迭代误差为$0.000000005427336$,符合预设精度要求,而且比二分法的最终迭代误差更小,显然可以发现弦截法的收敛速度要快于二分法。下面我再用弦截法的改造方法Steffensen方法进行试验。

Steffensen方法

根据Steffensen方法的计算方法,编写函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function [errors, ans, time] = steffensen(x, max, stopError)
%Steffensen方法
%func是待求零点的函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
f = func(x);
for i = 1 : max
o = x - f ^ 2 / (f - func(x - f));
error = abs(x - o);
x = o;
f = func(o);
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(o); %转换成角度
end

选取初始点为$0$,输入命令[errorsS1, ans, time] = steffensen(0, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsS1 =

0.381516143583955 0.018291709371805 0.000042893415627 0.000000000236814


ans =

22.909760215804820


time =

4

发现计算次数没有比弦截法少,因此修改初值为$\frac{\pi }{6}$,再次计算,得结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsS2 =

0.125855705283236 0.002107105082521 0.000000571210576


ans =

22.909760215802425


time =

3

一般而言,Steffensen方法的收敛速度要快于弦截法,但这与初始点的选取有关。对于这个问题,当设初始点为$0$时,Steffensen方法的迭代次数与弦截法持平;当设初始点为$\frac{\pi }{6}$时,迭代次数才小于弦截法。因此,想让Steffensen方法更快地收敛,需选取合适的初始点。在我看来,Steffensen方法的一大优势就是它是一种单步迭代方法,相比二步迭代方法的弦截法,Steffensen方法只需要一个初值就可以开始迭代。

Picard迭代法

上述的方法都是基于几何图形的求解方法,而下面的Picard迭代法则是基于不动点原理给出的。
首先,我们编写迭代函数:

1
2
3
4
5
6
function [y] = interation(x)
%迭代函数
%统一使用弧度制
format long %为提高精度,保留更多位数
y = asin(1 / (1.878 + 0.75 * cos(x)));
end

我们使用如下命令查看函数图像:

1
2
3
>> x = 0 : pi / 36 : pi / 6;
>> y = asin(1 * (1.878 + 0.75 * cos(x)) .^ (-1));
>> plot(x, y)

得到:

易验证,该函数在$[0,\frac{\pi }{6}]$上满足Picard迭代条件。

Picard迭代法

根据Picard迭代法原理,定义Picard迭代函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function [errors, ans, time] = picard(x, max, stopError)
%Picard迭代法
%interation是迭代函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
o = interation(x);
error = abs(x - o);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(o); %转换成角度
end

输入命令[errorsP, ans, time] = picard(0, 50, 1.7e-5)求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsP =

0.390355832559508 0.009044503640668 0.000428788831489 0.000020583068808 0.000000988625736


ans =

22.909757357777192


time =

5

Picard迭代结果为$22.909757357777192^{\circ}$,且精度符合要求。下面使用Picard迭代法的改进Aitken加速迭代法进行试验。

Aitken加速迭代法

编写Aitken加速迭代法函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function [errors, ans, time] = aitken(x, max, stopError)
%Aitken迭代法
%interation是迭代函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
x1 = interation(x);
x2 = interation(x1);
x3 = x2 - (x2 - x1) ^ 2 / (x2 - 2 * x1 + x);
error = abs(x3 - x);
x = x3;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

输入命令[errorsA, ans, time] = aitken(0, 50, 1.7e-5)求解得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsA =

0.399614867056990 0.000235879375045 0.000000000176165


ans =

22.909760215804827


time =

3

可以看到,Aitken加速迭代法仅需3次迭代就得到了符合条件的解,且它的迭代误差只用$0.000000000176165$,小于上述所有的方法,由此该方法的优势得以体现。

Newton迭代法

Newton迭代法也是一种求解非线性方程的高效算法,因此我也对其进行实现。
这里要用到func的导数,经计算,编写func的导函数为程序df

1
2
3
4
5
6
function [y] = df(x)
%求导数
%统一使用弧度制
format long
y = (0.75 * sin(x) / (1 - (1 / (1.878 + 0.75 * cos(x))) ^ 2) ^ 0.5) / (1.878 + 0.75 * cos(x)) ^ 2 - 1;
end

Newton迭代法

编写Newton迭代法的计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function [errors, ans, time] = newton(x, max, stopError)
%Newton迭代法
%func是待求零点的函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
o = x - func(x) / df(x);
error = abs(o - x);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

输入命令[errorsN, ans, time] = newton(0, 50, 1.7e-5)进行计算,得解:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsN =

0.390355832559508 0.009488988787132 0.000005925259246


ans =

22.909760215672179


time =

3

Newton下山法

为尽可能避免因初值选取不当导致计算过程缓慢收敛或者发散(经上面计算,此问题不存在这种情况),引入下山因子$\lambda \in (0,1]$,得到改进后的Newton下山法,其计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function [errors, ans, time] = hill(x, max, stopError)
%Newton下山法
% 此处显示详细说明
format long %为提高精度,保留更多位数
l = 1;
for i = 1 : max
o = x - l * func(x) / df(x);
while abs(func(o)) > abs(func(x)) %不满足下山条件
l = l / 2;
o = x - l * func(x) / df(x);
end
error = abs(o - x);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

计算得到结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsH =

0.390355832559508 0.009488988787132 0.000005925259246


ans =

22.909760215672179


time =

3

由于此问题初值选取得当,即初值取$0$时使用一般Newton迭代法不存在缓慢收敛或者发散的问题,因此Newton下山法在这里并没有发挥作用。可以看到,Newton迭代法仅$3$步就完成了求解的任务,非常高效。


问题的解

以上方法都得到了一致的答案,根据精度要求,我们得出此条件下功率角$\theta$为$22.910^{\circ}$,与许实章编《电机学习题集》中的例题答案$22.9^{\circ}$相吻合。


方法比较

将上述各种方法的误差记录绘制成图表,由于Newton下山法在该问题中没有发挥作用,因此仅作出Newton迭代法的迭代误差图像。

根据图片,我们可以观察到以上各个方法均收敛。其中,表现较为优越的有Aitken加速迭代法、Newton迭代法和Steffensen迭代法,均用了$3$次迭代就达到了精度要求。然而这里Steffensen法的收敛速度更依赖于初值的选取,当初值选为$0$时,它的收敛速度就类似于弦截法在该问题上的收敛速度了。因此,总的来说,对于这个问题,最高效的算法是Aitken加速迭代法和Newton迭代法。另外,二分法最简单却也最低效,迭代了$15$次才达到预设的精度要求。可见,各种算法的效率大致上与它们的复杂度和高级程度成正比关系。

]]>
+

上周科学计算引论结课了,借着写上机报告的机会,我把书本上所有的求解非线性方程的方法(标量型)都用matlab实现了一下,并对一个实际工程问题进行求解。关于实现过程中遇到的问题以及注意事项,我已写在matlab笔记:久未使用之后踩的一堆坑内。


实际问题

在电机学中,凸极同步发电机的功角特性可表示为:

式中,$P_{em}$表示发电机的电磁功率;$E_{0}$表示发电机电势;$V$表示发电机端电压;$x_{q}$表示横轴同步电抗;$x_{d}$表示纵轴同步电抗;$\theta$表示功率角,$\theta \in \left ( 0,\frac{\pi }{2} \right )$。
如令$\frac{E_{0}V}{x_{d}}=P_{j}$,$V^{2}\left ( \frac{1}{x_{q}}-\frac{1}{x_{d}} \right )=P_{2e}$,则上式可以简化为:

在电力系统稳定计算中,我们往往要由上式求出功率角$\theta$。我们可以使用几何方法求解,也可以利用迭代法求解该非线性方程。
我们将上式变为:

以许实章编《电机学习题集》第367页26-1为例,将$P_{em}=1$,$P_{j}=1.878$,$P_{2e}=0.75$代入,得到方程:


问题求解

在《计算方法》第二章,我们学习了一些非线性方程的数值解法,这里我们分别使用几何法和迭代法求解上述问题并进行比较。设方程求解的预定精度为$0.001^{\circ}$即$1.7\times 10^{-3}rad$,由闭区间上连续函数的性质和初步估计,可确定方程的解位于区间$[0,\frac{\pi }{6}]$。

几何方法

由几何法的求解方法,我们定义函数:

求解$\theta$即求解$f(\theta )$在$[0,\frac{\pi }{6}]$上的零点。
在matlab中,将该函数实现如下:

1
2
3
4
5
6
function [y] = func(x)
%几何法函数方程
%统一使用弧度制
format long %为提高精度,保留更多位数
y = asin(1 / (1.878 + 0.75 * cos(x))) - x;
end

由于matlab计算过程中默认保留$4$位小数,为了提高精度,我使用了format long保留更多有效数字。值得注意的是,计算时统一使用弧度制,待求出解之后再使用rad2deg函数转化为角度。

二分法

根据二分法的格式,编写二分法函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function [errors, ans, time] = bisection(low, high, max, stopError) 
%二分法
%func是待求零点的函数
%low,high分别是解区间的上下限
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
fl = func(low);
fh = func(high);
error = high - low;
for i = 1 : max
mid = (low + high ) / 2;
fm = func(mid);
if fm * fl > 0
low = mid;
else high = mid;
end
error = high - low;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(mid); %转换成角度
end

输入命令[errorsB, ans, time] = bisection(0, pi / 6, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
errorsB =

列 1 至 5

0.261799387799149 0.130899693899575 0.065449846949787 0.032724923474894 0.016362461737447

列 6 至 10

0.008181230868723 0.004090615434362 0.002045307717181 0.001022653858590 0.000511326929295

列 11 至 15

0.000255663464648 0.000127831732324 0.000063915866162 0.000031957933081 0.000015978966540


ans =

22.909240722656250


time =

15

使用二分法共迭代$15$次,求得结果为$22.909240722656250^{\circ}$。此外,迭代误差为$0.000015978966540$,符合预设精度要求。然而,此种方法计算次数较多,因此我又尝试了下面的方法。

弦截法

根据弦截法的计算方法,编写弦截法函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function [errors, ans, time] = linecut(a, b, max, stopError)
%弦截法
%func是待求零点的函数
%a,b是在解的领域取的两点,这里取解区间的两个端点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
fa = func(a);
fb = func(b);
error = abs(a - b);
for i = 1 : max
x = b - (b - a) * fb / (fb - fa);
a = b;
b = x;
fa = func(a);
fb = func(b);
error = abs(a - b);
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(b); %转换成角度
end

输入命令[errorsL, ans, time] = linecut(0, pi / 6, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsL =

0.120609794441825 0.003164447033051 0.000026217912123 0.000000005427336


ans =

22.909760215805360


time =

4

与上面的二分法相比,弦截法只需计算$4$次,效率大大提高。计算所得结果为$22.909760215805360^{\circ}$,迭代误差为$0.000000005427336$,符合预设精度要求,而且比二分法的最终迭代误差更小,显然可以发现弦截法的收敛速度要快于二分法。下面我再用弦截法的改造方法Steffensen方法进行试验。

Steffensen方法

根据Steffensen方法的计算方法,编写函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function [errors, ans, time] = steffensen(x, max, stopError)
%Steffensen方法
%func是待求零点的函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
f = func(x);
for i = 1 : max
o = x - f ^ 2 / (f - func(x - f));
error = abs(x - o);
x = o;
f = func(o);
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(o); %转换成角度
end

选取初始点为$0$,输入命令[errorsS1, ans, time] = steffensen(0, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsS1 =

0.381516143583955 0.018291709371805 0.000042893415627 0.000000000236814


ans =

22.909760215804820


time =

4

发现计算次数没有比弦截法少,因此修改初值为$\frac{\pi }{6}$,再次计算,得结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsS2 =

0.125855705283236 0.002107105082521 0.000000571210576


ans =

22.909760215802425


time =

3

一般而言,Steffensen方法的收敛速度要快于弦截法,但这与初始点的选取有关。对于这个问题,当设初始点为$0$时,Steffensen方法的迭代次数与弦截法持平;当设初始点为$\frac{\pi }{6}$时,迭代次数才小于弦截法。因此,想让Steffensen方法更快地收敛,需选取合适的初始点。在我看来,Steffensen方法的一大优势就是它是一种单步迭代方法,相比二步迭代方法的弦截法,Steffensen方法只需要一个初值就可以开始迭代。

Picard迭代法

上述的方法都是基于几何图形的求解方法,而下面的Picard迭代法则是基于不动点原理给出的。
首先,我们编写迭代函数:

1
2
3
4
5
6
function [y] = interation(x)
%迭代函数
%统一使用弧度制
format long %为提高精度,保留更多位数
y = asin(1 / (1.878 + 0.75 * cos(x)));
end

我们使用如下命令查看函数图像:

1
2
3
>> x = 0 : pi / 36 : pi / 6;
>> y = asin(1 * (1.878 + 0.75 * cos(x)) .^ (-1));
>> plot(x, y)

得到:

易验证,该函数在$[0,\frac{\pi }{6}]$上满足Picard迭代条件。

Picard迭代法

根据Picard迭代法原理,定义Picard迭代函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function [errors, ans, time] = picard(x, max, stopError)
%Picard迭代法
%interation是迭代函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
o = interation(x);
error = abs(x - o);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(o); %转换成角度
end

输入命令[errorsP, ans, time] = picard(0, 50, 1.7e-5)求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsP =

0.390355832559508 0.009044503640668 0.000428788831489 0.000020583068808 0.000000988625736


ans =

22.909757357777192


time =

5

Picard迭代结果为$22.909757357777192^{\circ}$,且精度符合要求。下面使用Picard迭代法的改进Aitken加速迭代法进行试验。

Aitken加速迭代法

编写Aitken加速迭代法函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function [errors, ans, time] = aitken(x, max, stopError)
%Aitken迭代法
%interation是迭代函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
x1 = interation(x);
x2 = interation(x1);
x3 = x2 - (x2 - x1) ^ 2 / (x2 - 2 * x1 + x);
error = abs(x3 - x);
x = x3;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

输入命令[errorsA, ans, time] = aitken(0, 50, 1.7e-5)求解得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsA =

0.399614867056990 0.000235879375045 0.000000000176165


ans =

22.909760215804827


time =

3

可以看到,Aitken加速迭代法仅需3次迭代就得到了符合条件的解,且它的迭代误差只用$0.000000000176165$,小于上述所有的方法,由此该方法的优势得以体现。

Newton迭代法

Newton迭代法也是一种求解非线性方程的高效算法,因此我也对其进行实现。
这里要用到func的导数,经计算,编写func的导函数为程序df

1
2
3
4
5
6
function [y] = df(x)
%求导数
%统一使用弧度制
format long
y = (0.75 * sin(x) / (1 - (1 / (1.878 + 0.75 * cos(x))) ^ 2) ^ 0.5) / (1.878 + 0.75 * cos(x)) ^ 2 - 1;
end

Newton迭代法

编写Newton迭代法的计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function [errors, ans, time] = newton(x, max, stopError)
%Newton迭代法
%func是待求零点的函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
o = x - func(x) / df(x);
error = abs(o - x);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

输入命令[errorsN, ans, time] = newton(0, 50, 1.7e-5)进行计算,得解:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsN =

0.390355832559508 0.009488988787132 0.000005925259246


ans =

22.909760215672179


time =

3

Newton下山法

为尽可能避免因初值选取不当导致计算过程缓慢收敛或者发散(经上面计算,此问题不存在这种情况),引入下山因子$\lambda \in (0,1]$,得到改进后的Newton下山法,其计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function [errors, ans, time] = hill(x, max, stopError)
%Newton下山法
% 此处显示详细说明
format long %为提高精度,保留更多位数
l = 1;
for i = 1 : max
o = x - l * func(x) / df(x);
while abs(func(o)) > abs(func(x)) %不满足下山条件
l = l / 2;
o = x - l * func(x) / df(x);
end
error = abs(o - x);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

计算得到结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsH =

0.390355832559508 0.009488988787132 0.000005925259246


ans =

22.909760215672179


time =

3

由于此问题初值选取得当,即初值取$0$时使用一般Newton迭代法不存在缓慢收敛或者发散的问题,因此Newton下山法在这里并没有发挥作用。可以看到,Newton迭代法仅$3$步就完成了求解的任务,非常高效。


问题的解

以上方法都得到了一致的答案,根据精度要求,我们得出此条件下功率角$\theta$为$22.910^{\circ}$,与许实章编《电机学习题集》中的例题答案$22.9^{\circ}$相吻合。


方法比较

将上述各种方法的误差记录绘制成图表,由于Newton下山法在该问题中没有发挥作用,因此仅作出Newton迭代法的迭代误差图像。

根据图片,我们可以观察到以上各个方法均收敛。其中,表现较为优越的有Aitken加速迭代法、Newton迭代法和Steffensen迭代法,均用了$3$次迭代就达到了精度要求。然而这里Steffensen法的收敛速度更依赖于初值的选取,当初值选为$0$时,它的收敛速度就类似于弦截法在该问题上的收敛速度了。因此,总的来说,对于这个问题,最高效的算法是Aitken加速迭代法和Newton迭代法。另外,二分法最简单却也最低效,迭代了$15$次才达到预设的精度要求。可见,各种算法的效率大致上与它们的复杂度和高级程度成正比关系。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>上周科学计算引论结课了,借着写上机报告的机会,我把书本上所有的求解非线性方程的方法(标量型)都用matlab实现了一下,并对一个实际工程问题进行 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>上周科学计算引论结课了,借着写上机报告的机会,我把书本上所有的求解非线性方程的方法(标量型)都用matlab实现了一下,并对一个实际工程问题进行 @@ -1509,13 +1509,13 @@ 2019-11-15T15:31:16.000Z 2020-01-19T00:27:14.020Z -

本周数字电路课程老师布置了一个利用verilog语言进行数值比较器波形仿真的作业。可以利用Modelsim或者Vivado实现。由于Vivado默认安装大小就有将近30个GB(2018版好像是27GB左右,2014版是12.68GB,这版本的容量增速跟maltab有得一拼啊),因此之前装了之后不太会使用便又卸了。最近刚好趁着双十一降价给自己的laptop加了一个SSD,因此正好赶快学习一下如何使用。有关如何给笔记本加装SSD的问题,这里有两个视频可以解决,安装准备与步骤安装后点亮磁盘

References

电子文献:
https://blog.csdn.net/qq_41154156/article/details/80989125
https://wenku.baidu.com/view/0294cbb3bb4cf7ec4bfed01a.html


关于vivado

相比于Modelsim,Vivado的UI还是要舒服许多的,有点像Multisim之于Pspice。关于Vivado的使用,上面参考的文章中的步骤比较详细,照做一遍之后基本就会了。


1位数值比较器

1位数值比较器的逻辑图如下:

使用verilog代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module _1bit_Comp(
input A,
input B,
output AGB,
output AEB,
output ALB
);
wire Anot, Bnot;
not n0(Anot, A),
n1(Bnot, B);
and n2(AGB, A, Bnot),
n3(ALB, Anot, B);
nor n4(AEB, AGB, ALB);
endmodule

为了输出仿真波形,新建一个仿真文件:

1
2
3
4
5
6
7
8
9
module simulateFile();
reg A, B;
wire AGB, AEB, ALB;
_1bit_Comp u1(A, B, AGB, AEB, ALB);
initial
begin A = 0; B = 0;
end
always #50 {A, B} = {A, B} + 1;
endmodule

其中,过程赋值语句always只能给寄存器类型变量赋值,因此,在这里A、B要定义为reg类型。
这里“#50”表示延时,使用{A, B}使AB变成二进制数,方便生成所有不同的输入,在这里即00、01、10、11。
Run Simulation,输出波形:


2位数值比较器

2位数值比较器的逻辑图如下:

使用verilog代码,调用1位数值比较器,实现2位数值比较器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module _2bit_Comp(
input A1,
input A0,
input B1,
input B0,
output FAGB,
output FAEB,
output FALB
);
wire AGB1, AEB1, ALB1, AGB0, AEB0, ALB0; //signal inside
wire G1O, G2O; //the output of and gate G1, G2
_1bit_Comp C1(A1, B1, AGB1, AEB1, ALB1); //Instantiate 1-bit Comparator
_1bit_Comp C0(A0, B0, AGB0, AEB0, ALB0);
and G1(G1O, AEB1, AGB0),
G2(G2O, AEB1, ALB0),
G3(FAEB, AEB1, AEB0);
or G4(FAGB, AGB1, G1O);
or G5(FALB, ALB1, G2O);
endmodule

可以使用RTL ANALYSIS来仿真出2位数值比较器的RTL schematic电子原理图。

类似的,编写仿真文件:

1
2
3
4
5
6
7
8
9
module simulateAgain();
reg A1, A0, B1, B0;
wire FAGB, FAEB, FALB;
_2bit_Comp u2(A1, A0, B1, B0, FAGB, FAEB, FALB);
initial
begin A1 = 0; A0 = 0; B1 = 0; B0 = 0;
end
always #30 {A1, A0, B1, B0} = {A1, A0, B1, B0} + 1;
endmodule

Run Simulation,输出波形:


出现的问题

  1. 报错:[Synth 8-439] module’xxx’not found

    当初遇到这个问题后,我的第一反应是上网搜索原因。得到的解释有模块未添加、IP未正确设置等。
    对照网上的解决方案之后,我发现除了我无法理解的,网上所述的问题我都不存在。于是我只好独立进行思考。
    果然,我还犯不了像网上那样“高级”的错误。错误的原代码如下:

    1
    2
    not n0(Anot, A);
    n1(Bnot, B);

    报错:[Synth 8-439] module’n1’not found。
    当我调用门的时候,由于内部变量换行导致我将逗号误用成了分号,因此导致分号之后的变量not found,修改后错误即可解决。
    其实,这个问题仔细观察即可发觉,相比于n1,同样格式的n0就没有报错,那么很有可能错误就在两者之间。

  2. ERROR: [Common 17-39] ‘xxx’ failed due to earlier errors

    这是我在执行仿真文件时遇到的error。仔细检查后,发现错误也与上一个问题相同。由于我在设计完电路后没有Run Synthesis综合并生成网表文件来进行检验,也没有进行其它的仿真操作,因此之前并没能发现这个问题。于是最后当调用该电路的仿真文件开始运行时就会报告这样的错误。

要注意的点

  1. 和matlab中函数文件的要求类似,verilog定义模块时,需要新建的模块文件名称与模块的文件名称一致。例如,我上面的1位数值比较器module名为_1bit_Comp,那么对应的文件名就应该是_1bit_Comp.v。此外,每个模块应使用一个文件来表示,且一个文件最多能表示一个模块(可以在其中调用其它模块,这点和matlab很像),两者呈一一对应关系。
  2. 新建project时,如要从RTL代码开始综合,就选择RTL project(默认的这个)。要注意的是,下面的“Do not specify sources at this time”(此时不定义源文件)可以勾上。否则,下一步会进入添加source file。
  3. 如果在一个project中已经建立了一个仿真文件,那么当你新建一个仿真文件时,需要建立在create的new file内,这样在后面对不同的仿真文件进行仿真时可以将对应的文件夹依次分别激活。
  4. 在source窗口中,一般情况下,Vivado会自动加粗识别出来的top module,同时对应module名称前面也会有一个二叉树状的图标表示这是顶层模块。有时候,软件也会识别错误或者与实际需求不符,这时候我们可以右键想要置顶的module,在弹出的菜单中点击Set As Top将其设为顶层。
  5. 当在同一个project中创建了多个仿真文件时,如要在进行完一次仿真之后对另一个仿真文件,需要对对应的文件夹进行激活。方法是右键仿真文件,然后在弹出的菜单中点击Make Active即可。

加装固态盘

前文提到给笔记本加装SSD,给了两个示范视频,这里我还是想再稍稍补充一下关于加装固态盘的一些事情。
首先必须确保自己的本有空位。我使用的是小电池版本,因此有一整个2.5英寸7mm的硬盘位,这个请在决定购买新的硬盘前和卖家自己核对确认。如果还是不放心,那么最好亲自拆开查看,眼见为实嘛。要注意的是,必须使用完全对应规格的螺丝刀(比如我使用的是梅花T5螺丝刀),否则很容易发生滑丝,即螺牙连接处由于受力过大或其它原因导致螺牙磨损而使螺牙无法咬合,这会为今后的拆机带来不必要的麻烦。


上面是我买的固态盘和数据线,相同或者类似机型的可以参考一下。在到货之后我发现,我所买的闪迪SSD较7mm稍薄些,因此平时拿动时(一般较大幅度翻动laptop时)会感到里面有东西松动的响声,不过使用至今没遇到任何问题。另外,为了适配各类硬盘,数据线的长度也有可能不能完全匹配,其实也没有关系,稍稍用力将数据线对应接头按入SATA3接口并用架子将固态盘固定即可。相比移动硬盘的USB接口,内置固态的SATA3读取速度还是相当不错的。

]]>
+

本周数字电路课程老师布置了一个利用verilog语言进行数值比较器波形仿真的作业。可以利用Modelsim或者Vivado实现。由于Vivado默认安装大小就有将近30个GB(2018版好像是27GB左右,2014版是12.68GB,这版本的容量增速跟maltab有得一拼啊),因此之前装了之后不太会使用便又卸了。最近刚好趁着双十一降价给自己的laptop加了一个SSD,因此正好赶快学习一下如何使用。有关如何给笔记本加装SSD的问题,这里有两个视频可以解决,安装准备与步骤安装后点亮磁盘

References

电子文献:
https://blog.csdn.net/qq_41154156/article/details/80989125
https://wenku.baidu.com/view/0294cbb3bb4cf7ec4bfed01a.html


关于vivado

相比于Modelsim,Vivado的UI还是要舒服许多的,有点像Multisim之于Pspice。关于Vivado的使用,上面参考的文章中的步骤比较详细,照做一遍之后基本就会了。


1位数值比较器

1位数值比较器的逻辑图如下:

使用verilog代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module _1bit_Comp(
input A,
input B,
output AGB,
output AEB,
output ALB
);
wire Anot, Bnot;
not n0(Anot, A),
n1(Bnot, B);
and n2(AGB, A, Bnot),
n3(ALB, Anot, B);
nor n4(AEB, AGB, ALB);
endmodule

为了输出仿真波形,新建一个仿真文件:

1
2
3
4
5
6
7
8
9
module simulateFile();
reg A, B;
wire AGB, AEB, ALB;
_1bit_Comp u1(A, B, AGB, AEB, ALB);
initial
begin A = 0; B = 0;
end
always #50 {A, B} = {A, B} + 1;
endmodule

其中,过程赋值语句always只能给寄存器类型变量赋值,因此,在这里A、B要定义为reg类型。
这里“#50”表示延时,使用{A, B}使AB变成二进制数,方便生成所有不同的输入,在这里即00、01、10、11。
Run Simulation,输出波形:


2位数值比较器

2位数值比较器的逻辑图如下:

使用verilog代码,调用1位数值比较器,实现2位数值比较器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module _2bit_Comp(
input A1,
input A0,
input B1,
input B0,
output FAGB,
output FAEB,
output FALB
);
wire AGB1, AEB1, ALB1, AGB0, AEB0, ALB0; //signal inside
wire G1O, G2O; //the output of and gate G1, G2
_1bit_Comp C1(A1, B1, AGB1, AEB1, ALB1); //Instantiate 1-bit Comparator
_1bit_Comp C0(A0, B0, AGB0, AEB0, ALB0);
and G1(G1O, AEB1, AGB0),
G2(G2O, AEB1, ALB0),
G3(FAEB, AEB1, AEB0);
or G4(FAGB, AGB1, G1O);
or G5(FALB, ALB1, G2O);
endmodule

可以使用RTL ANALYSIS来仿真出2位数值比较器的RTL schematic电子原理图。

类似的,编写仿真文件:

1
2
3
4
5
6
7
8
9
module simulateAgain();
reg A1, A0, B1, B0;
wire FAGB, FAEB, FALB;
_2bit_Comp u2(A1, A0, B1, B0, FAGB, FAEB, FALB);
initial
begin A1 = 0; A0 = 0; B1 = 0; B0 = 0;
end
always #30 {A1, A0, B1, B0} = {A1, A0, B1, B0} + 1;
endmodule

Run Simulation,输出波形:


出现的问题

  1. 报错:[Synth 8-439] module’xxx’not found

    当初遇到这个问题后,我的第一反应是上网搜索原因。得到的解释有模块未添加、IP未正确设置等。
    对照网上的解决方案之后,我发现除了我无法理解的,网上所述的问题我都不存在。于是我只好独立进行思考。
    果然,我还犯不了像网上那样“高级”的错误。错误的原代码如下:

    1
    2
    not n0(Anot, A);
    n1(Bnot, B);

    报错:[Synth 8-439] module’n1’not found。
    当我调用门的时候,由于内部变量换行导致我将逗号误用成了分号,因此导致分号之后的变量not found,修改后错误即可解决。
    其实,这个问题仔细观察即可发觉,相比于n1,同样格式的n0就没有报错,那么很有可能错误就在两者之间。

  2. ERROR: [Common 17-39] ‘xxx’ failed due to earlier errors

    这是我在执行仿真文件时遇到的error。仔细检查后,发现错误也与上一个问题相同。由于我在设计完电路后没有Run Synthesis综合并生成网表文件来进行检验,也没有进行其它的仿真操作,因此之前并没能发现这个问题。于是最后当调用该电路的仿真文件开始运行时就会报告这样的错误。

要注意的点

  1. 和matlab中函数文件的要求类似,verilog定义模块时,需要新建的模块文件名称与模块的文件名称一致。例如,我上面的1位数值比较器module名为_1bit_Comp,那么对应的文件名就应该是_1bit_Comp.v。此外,每个模块应使用一个文件来表示,且一个文件最多能表示一个模块(可以在其中调用其它模块,这点和matlab很像),两者呈一一对应关系。
  2. 新建project时,如要从RTL代码开始综合,就选择RTL project(默认的这个)。要注意的是,下面的“Do not specify sources at this time”(此时不定义源文件)可以勾上。否则,下一步会进入添加source file。
  3. 如果在一个project中已经建立了一个仿真文件,那么当你新建一个仿真文件时,需要建立在create的new file内,这样在后面对不同的仿真文件进行仿真时可以将对应的文件夹依次分别激活。
  4. 在source窗口中,一般情况下,Vivado会自动加粗识别出来的top module,同时对应module名称前面也会有一个二叉树状的图标表示这是顶层模块。有时候,软件也会识别错误或者与实际需求不符,这时候我们可以右键想要置顶的module,在弹出的菜单中点击Set As Top将其设为顶层。
  5. 当在同一个project中创建了多个仿真文件时,如要在进行完一次仿真之后对另一个仿真文件,需要对对应的文件夹进行激活。方法是右键仿真文件,然后在弹出的菜单中点击Make Active即可。

加装固态盘

前文提到给笔记本加装SSD,给了两个示范视频,这里我还是想再稍稍补充一下关于加装固态盘的一些事情。
首先必须确保自己的本有空位。我使用的是小电池版本,因此有一整个2.5英寸7mm的硬盘位,这个请在决定购买新的硬盘前和卖家自己核对确认。如果还是不放心,那么最好亲自拆开查看,眼见为实嘛。要注意的是,必须使用完全对应规格的螺丝刀(比如我使用的是梅花T5螺丝刀),否则很容易发生滑丝,即螺牙连接处由于受力过大或其它原因导致螺牙磨损而使螺牙无法咬合,这会为今后的拆机带来不必要的麻烦。


上面是我买的固态盘和数据线,相同或者类似机型的可以参考一下。在到货之后我发现,我所买的闪迪SSD较7mm稍薄些,因此平时拿动时(一般较大幅度翻动laptop时)会感到里面有东西松动的响声,不过使用至今没遇到任何问题。另外,为了适配各类硬盘,数据线的长度也有可能不能完全匹配,其实也没有关系,稍稍用力将数据线对应接头按入SATA3接口并用架子将固态盘固定即可。相比移动硬盘的USB接口,内置固态的SATA3读取速度还是相当不错的。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>本周数字电路课程老师布置了一个利用verilog语言进行数值比较器波形仿真的作业。可以利用Modelsim或者Vivado实现。由于Vivado + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>本周数字电路课程老师布置了一个利用verilog语言进行数值比较器波形仿真的作业。可以利用Modelsim或者Vivado实现。由于Vivado @@ -1526,12 +1526,12 @@ - - + +
@@ -1543,13 +1543,13 @@ 2019-11-10T11:42:56.000Z 2020-02-07T09:32:51.929Z -

在上一篇matlab笔记:久未使用之后踩的一堆坑中,我用matlab完成了实验,接下来就是写报告了。然而写博客用惯了markdown,现在极其嫌弃word文档。考虑到这次报告中大量的公式及代码,word文档的观感可想而知,因此果断决定使用markdown来书写实验报告。那么这篇文章就及时跟进一下我在写报告的时候发现的一些之前没注意的新应用吧。

References

电子文献:
https://blog.csdn.net/m0_37925202/article/details/80461714
https://www.w3school.com.cn/tags/att_p_align.asp


居中文字

之前写markdown的时候一直没有考虑到要把文字居中,而这回报告的标题就有这个要求了。
实现的方法有如下两种。

1
<center>这样就居中了</center>

1
<p align="center">这样就居中了</p>

此外html中的<p>标签的align属性还有其它的用法:

1
2
3
4
<p align="left"> <!--左对齐-->
<p align="right"> <!--右对齐-->
<p align="center"> <!--居中-->
<p align="justify"> <!--对行进行伸展,使每行都可以有相等的长度(就像在报纸和杂志中)-->


转pdf文件

考虑到markdown文件不能作为最终的报告提交,我就想办法将markdown转化为pdf。
我首先找到了VScode里面的markdown PDF扩展,然而安装之后有一个东西一直安装失败。
这里我想起了之前我在markdown笔记:markdown的基本使用中强烈推荐过的typora(我的报告最后就是用它书写的),打开之后,果然没有让我失望。直接点击“文件”“导出”选择PDF,瞬间就完成了pdf文件的转换。此外,typora提供的导出格式实在是丰富,虽然导出的word与原markdown文件的颜值有少许降低,但还是不得不赞叹typora的实用!


行间公式

刚开始使用typora时,我遇到了一个疑惑:似乎typora不能插入行内公式块。在查找相关资料之后,我终于找到了解决的方案。
点击“文件”“偏好设置”,把markdown扩展语法中的内联公式项打上勾。

补充:当使用$夹着文字而非LaTex格式的公式时,字体会发生变化,如输入$我会变$我没变,呈现的效果是:$我会变$我没变。


插入图片

typora插入图片的方法与hexo中类似,也是可以创建一个文件夹存放图片,然后在偏好设置里进行设置。当指定路径之后,typora存放图片的文件夹名可以与markdown文件的名字不一致,而hexo中则需要一致才能够直接用图片名调用。

此外,还是在偏好设置中,我们可以调整字体,我一般使用的是16px。

]]>
+

在上一篇matlab笔记:久未使用之后踩的一堆坑中,我用matlab完成了实验,接下来就是写报告了。然而写博客用惯了markdown,现在极其嫌弃word文档。考虑到这次报告中大量的公式及代码,word文档的观感可想而知,因此果断决定使用markdown来书写实验报告。那么这篇文章就及时跟进一下我在写报告的时候发现的一些之前没注意的新应用吧。

References

电子文献:
https://blog.csdn.net/m0_37925202/article/details/80461714
https://www.w3school.com.cn/tags/att_p_align.asp


居中文字

之前写markdown的时候一直没有考虑到要把文字居中,而这回报告的标题就有这个要求了。
实现的方法有如下两种。

1
<center>这样就居中了</center>

1
<p align="center">这样就居中了</p>

此外html中的<p>标签的align属性还有其它的用法:

1
2
3
4
<p align="left"> <!--左对齐-->
<p align="right"> <!--右对齐-->
<p align="center"> <!--居中-->
<p align="justify"> <!--对行进行伸展,使每行都可以有相等的长度(就像在报纸和杂志中)-->


转pdf文件

考虑到markdown文件不能作为最终的报告提交,我就想办法将markdown转化为pdf。
我首先找到了VScode里面的markdown PDF扩展,然而安装之后有一个东西一直安装失败。
这里我想起了之前我在markdown笔记:markdown的基本使用中强烈推荐过的typora(我的报告最后就是用它书写的),打开之后,果然没有让我失望。直接点击“文件”“导出”选择PDF,瞬间就完成了pdf文件的转换。此外,typora提供的导出格式实在是丰富,虽然导出的word与原markdown文件的颜值有少许降低,但还是不得不赞叹typora的实用!


行间公式

刚开始使用typora时,我遇到了一个疑惑:似乎typora不能插入行内公式块。在查找相关资料之后,我终于找到了解决的方案。
点击“文件”“偏好设置”,把markdown扩展语法中的内联公式项打上勾。

补充:当使用$夹着文字而非LaTex格式的公式时,字体会发生变化,如输入$我会变$我没变,呈现的效果是:$我会变$我没变。


插入图片

typora插入图片的方法与hexo中类似,也是可以创建一个文件夹存放图片,然后在偏好设置里进行设置。当指定路径之后,typora存放图片的文件夹名可以与markdown文件的名字不一致,而hexo中则需要一致才能够直接用图片名调用。

此外,还是在偏好设置中,我们可以调整字体,我一般使用的是16px。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>在上一篇<a href="https://gsy00517.github.io/matlab20191110193640/" target="_ + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在上一篇<a href="https://gsy00517.github.io/matlab20191110193640/" target="_ @@ -1569,13 +1569,13 @@ 2019-11-10T11:36:40.000Z 2020-02-07T09:31:48.498Z -

本周科学计算引论结课了,就花了一整天时间把要求的实验报告写了。根据考核说明,算法可以使用各种工具、语言来实现,但由于这门课程的上机实验统一使用的是matlab,再加上我上一次使用matlab大量实践是在劳动节的数学建模华中赛了,还是很有必要重拾起来再熟悉一下。于是乎,一大波坑就等着我去填了。

References

电子文献:
https://blog.csdn.net/zhanshen112/article/details/79728887


报错:未找到具有匹配签名的构造函数

我编写了一个二分法的函数求解非线性方程,然而当我调用的时候,却遇到报错:“未找到具有匹配签名的构造函数”,这是怎么回事呢?
我们来冷静分析一下。

识破!
原来是我定义的函数名为half,而half也是matlab自带的函数之一,可以使用help functionname查看函数具体的使用方法与功能。

调用时默认优先使用自带的函数,因此修改函数名为matlab自带函数之外的即可。


报错:矩阵维度必须一致

这是我在画图时遇到的一个问题,首先先补充一下matlab中作函数图像的方法,如下图所示。

当我尝试使用上述方法作简单的函数图像时,并没有报错,而当我想要作出我实验所需函数(如下所示)的图像时,却出现了“矩阵维度必须一致”的错误信息。

1
y = asin(1 / (1.878 + 0.75 * cos(x)))

查阅相关资料,我才发现是乘除的时候出现的问题,为此我将其中的除/修改为乘*,并用.^代替^作乘方计算,即将原函数式修改为:

1
y = asin(1 * (1.878 + 0.75 * cos(x)) .^ (-1))

这样,问题就解决了。
这里我想强调一下在matlab中^.^的区别:.^是点乘,而^是乘法。
直接用^进行乘法的话,在这里即矩阵乘法,也就是说,必须满足前一个矩阵的列数等于后一个矩阵的行数。
而使用.^点乘操作,是使每一个元素相乘,也就是向量或者矩阵中对应元素相乘,也很好记忆,加个点就是点乘。


函数输出只有一个

这是一个很愚蠢的问题,显然我好久没用了,因为matlab不必使用return返回结果,在函数声明的第一句就确定了返回值的数量和顺序。因此在调用函数的时候,必须也提供对应的变量去接收返回值,否则只能得到第一个返回的元素。


使用diff求导不是导数值

用惯了pytorch,总想着能够自动求导,一查matlab还真有这么一个函数,即diff函数。然而,事实证明它不是我想要的。
我们可以在命令行中使用这一个函数:

  1. 声明变量x:syms x。它代表着声明符号变量x,只有声明了符号变量才可以进行符号运算,包括求导。
  2. 定义一个需要求导的函数:f(x) = sin(x) + x ^ 2
  3. 使用diff函数求导:diff(f(x))。也可以对已经定义好的m文件中的函数直接求导。
    这里,我们会得到ans为2*x + cos(x)
  4. 如果想pretty一些,可以使用pretty函数将结果转化成书面格式:pretty(ans)

然而,当我代入不同的具体数值想得到函数的导数值的时候,发现输出的结果却是0。
使用help查阅diff函数的用法,得到的说明是:

1
2
3
4
此MATLAB函数计算沿大小不等于1的第一个数组维度的X相邻元素之间的差分:
Y = diff(X)
Y = diff(X,n)
Y = diff(X,n,dim)

原来这个函数的主要用法是对向量或者矩阵中的元素进行差分计算,当用它来求导时,得到的只是一个表达式,且函数一但复杂,得到的就是一个参数众多的逼近格式。
唉,总而言之,还是老老实实地拿起笔自己算出导函数吧。都用矩阵实验室(matlab)了,手动求个导还是得会的呀。


角度制弧度制互换

无论使用计算器还是编程计算,这都是一个需要注意的点,matlab默认使用的是弧度制,在计算出结果之后,可以使用rad2deg函数进行转换。
同样的,我推测角度制转弧度制的函数名为deg2rad,一试,果不其然。这个函数还是挺好记的,英文里有许多同音词与字符的妙用,比如这里的to和2的two,还有at和@,感觉既方便又高级。


保留更多位数

matlab默认是保留4位有效数字,为了提升计算精度,可以使用format long来增加计算过程中保留的位数。


常用操作

难得开一篇写matlab使用的博文,那就在这补上几个我记忆中的较为常用的命令或操作。

  1. Ctrl+C终止操作。这跟许多地方都一样,在matlab中,Ctrl+C平时可以用来粘贴剪切板上的内容,而在程序运行时,可以使用它来终止运行,这在死循环的时候非常有用。
  2. clc清空命令行。
  3. bench测试性能。这其实不是一个很常用的命令,可以跟朋友输这个命令看看自己电脑的性能,下图是我的结果。需要注意的是,笔记本电脑电池使用模式的不同对这个排名影响还是挺大的,如果想让排名高一些的话,请确保电池开在最佳性能的模式。另外,不同的电脑的比较对象可能会不一样,比如学校的台式机和我的笔记本在这里比较的对象就不一样。 常用的还有许多,以后在使用中不断增加,先在这占个位。
    最近在b站看到matlab的几个有趣彩蛋,挺有意思,感兴趣可以去瞧瞧。
]]>
+

本周科学计算引论结课了,就花了一整天时间把要求的实验报告写了。根据考核说明,算法可以使用各种工具、语言来实现,但由于这门课程的上机实验统一使用的是matlab,再加上我上一次使用matlab大量实践是在劳动节的数学建模华中赛了,还是很有必要重拾起来再熟悉一下。于是乎,一大波坑就等着我去填了。

References

电子文献:
https://blog.csdn.net/zhanshen112/article/details/79728887


报错:未找到具有匹配签名的构造函数

我编写了一个二分法的函数求解非线性方程,然而当我调用的时候,却遇到报错:“未找到具有匹配签名的构造函数”,这是怎么回事呢?
我们来冷静分析一下。

识破!
原来是我定义的函数名为half,而half也是matlab自带的函数之一,可以使用help functionname查看函数具体的使用方法与功能。

调用时默认优先使用自带的函数,因此修改函数名为matlab自带函数之外的即可。


报错:矩阵维度必须一致

这是我在画图时遇到的一个问题,首先先补充一下matlab中作函数图像的方法,如下图所示。

当我尝试使用上述方法作简单的函数图像时,并没有报错,而当我想要作出我实验所需函数(如下所示)的图像时,却出现了“矩阵维度必须一致”的错误信息。

1
y = asin(1 / (1.878 + 0.75 * cos(x)))

查阅相关资料,我才发现是乘除的时候出现的问题,为此我将其中的除/修改为乘*,并用.^代替^作乘方计算,即将原函数式修改为:

1
y = asin(1 * (1.878 + 0.75 * cos(x)) .^ (-1))

这样,问题就解决了。
这里我想强调一下在matlab中^.^的区别:.^是点乘,而^是乘法。
直接用^进行乘法的话,在这里即矩阵乘法,也就是说,必须满足前一个矩阵的列数等于后一个矩阵的行数。
而使用.^点乘操作,是使每一个元素相乘,也就是向量或者矩阵中对应元素相乘,也很好记忆,加个点就是点乘。


函数输出只有一个

这是一个很愚蠢的问题,显然我好久没用了,因为matlab不必使用return返回结果,在函数声明的第一句就确定了返回值的数量和顺序。因此在调用函数的时候,必须也提供对应的变量去接收返回值,否则只能得到第一个返回的元素。


使用diff求导不是导数值

用惯了pytorch,总想着能够自动求导,一查matlab还真有这么一个函数,即diff函数。然而,事实证明它不是我想要的。
我们可以在命令行中使用这一个函数:

  1. 声明变量x:syms x。它代表着声明符号变量x,只有声明了符号变量才可以进行符号运算,包括求导。
  2. 定义一个需要求导的函数:f(x) = sin(x) + x ^ 2
  3. 使用diff函数求导:diff(f(x))。也可以对已经定义好的m文件中的函数直接求导。
    这里,我们会得到ans为2*x + cos(x)
  4. 如果想pretty一些,可以使用pretty函数将结果转化成书面格式:pretty(ans)

然而,当我代入不同的具体数值想得到函数的导数值的时候,发现输出的结果却是0。
使用help查阅diff函数的用法,得到的说明是:

1
2
3
4
此MATLAB函数计算沿大小不等于1的第一个数组维度的X相邻元素之间的差分:
Y = diff(X)
Y = diff(X,n)
Y = diff(X,n,dim)

原来这个函数的主要用法是对向量或者矩阵中的元素进行差分计算,当用它来求导时,得到的只是一个表达式,且函数一但复杂,得到的就是一个参数众多的逼近格式。
唉,总而言之,还是老老实实地拿起笔自己算出导函数吧。都用矩阵实验室(matlab)了,手动求个导还是得会的呀。


角度制弧度制互换

无论使用计算器还是编程计算,这都是一个需要注意的点,matlab默认使用的是弧度制,在计算出结果之后,可以使用rad2deg函数进行转换。
同样的,我推测角度制转弧度制的函数名为deg2rad,一试,果不其然。这个函数还是挺好记的,英文里有许多同音词与字符的妙用,比如这里的to和2的two,还有at和@,感觉既方便又高级。


保留更多位数

matlab默认是保留4位有效数字,为了提升计算精度,可以使用format long来增加计算过程中保留的位数。


常用操作

难得开一篇写matlab使用的博文,那就在这补上几个我记忆中的较为常用的命令或操作。

  1. Ctrl+C终止操作。这跟许多地方都一样,在matlab中,Ctrl+C平时可以用来粘贴剪切板上的内容,而在程序运行时,可以使用它来终止运行,这在死循环的时候非常有用。
  2. clc清空命令行。
  3. bench测试性能。这其实不是一个很常用的命令,可以跟朋友输这个命令看看自己电脑的性能,下图是我的结果。需要注意的是,笔记本电脑电池使用模式的不同对这个排名影响还是挺大的,如果想让排名高一些的话,请确保电池开在最佳性能的模式。另外,不同的电脑的比较对象可能会不一样,比如学校的台式机和我的笔记本在这里比较的对象就不一样。 常用的还有许多,以后在使用中不断增加,先在这占个位。
    最近在b站看到matlab的几个有趣彩蛋,挺有意思,感兴趣可以去瞧瞧。
]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>本周科学计算引论结课了,就花了一整天时间把要求的实验报告写了。根据考核说明,算法可以使用各种工具、语言来实现,但由于这门课程的上机实验统一使用的 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>本周科学计算引论结课了,就花了一整天时间把要求的实验报告写了。根据考核说明,算法可以使用各种工具、语言来实现,但由于这门课程的上机实验统一使用的 @@ -1597,13 +1597,13 @@ 2019-11-10T08:59:01.000Z 2020-02-18T04:43:15.496Z -

最近心血来潮花了好久给自己的博客添加了一个粒子时钟,最后想要使它在sidebar中居中显示废了我好大功夫,为了以后不在这上面浪费时间,我决定浪费现在的时间把这个问题记下来。

References

电子文献:
https://www.jianshu.com/p/f0bffc42c1ce
https://blog.csdn.net/chwshuang/article/details/52350559


swig

由于我要将我的时钟显示在侧边栏,需要插入到header.swig文件中。hexo博客的源码中有大量这个格式的文件,然而具体的使用方法我也不是很清楚。在查阅了一些文章之后,我对swig有了一个初步的认知。
swig是一个JS前端模板引擎,它有如下特点:

  • 支持大多数主流浏览器。
  • 表达式兼容性好。
  • 面向对象的模板继承。
  • 将过滤器和转换应用到模板中的输出。
  • 可根据路劲渲染页面。
  • 支持页面复用。
  • 支持动态页面。
  • 可扩展、可定制。

可以通过VSchart将swig与其它前端模板框架进行对比,这个网站由维基支持,可以在里面进行各种对比,非常有意思,在这里推荐一下。
关于swig的基本用法,可以在我文首的第一个参考链接中找到,个人认为不搞前端的话大概率是用不到的。


原本的方法

当我添加完时钟本地测验时,我发现添加的时钟在侧边栏的位置没有居中。

根据之前学习html的记忆,我尝试了使用<p align=center></p>标签包裹我的插入语句,然而并没有达到想要的效果。


使用div实现居中

为了达到上述的目的,我使用<div>标签来分割出块,并使用div的属性来实现居中显示的效果。

1
2
3
<div style="Text-align:center;width:100%;">
{% include '../_custom/clock.swig' %}
</div>


使用nav实现隐藏

本以为大功告成,结果在移动端查看时,发现竖屏显示效果非常煞风景。

其实在电脑端浏览器也可以预览移动端的效果,方法很简单,就是直接将浏览器窗口小化,减小两边间距,网页就会自动变成竖屏显示的状态(除非没有)。此外,当我们F12检查元素或者查看源时,网页也会被挤到一侧从而变成竖屏显示的状态。

注:这里补充一下检查元素和查看源之间的区别,一般的浏览器右键都会有这两个功能,表面上看起来似乎也差不多,但是它们还是有区别的。
检查元素看的是渲染过的最终代码,可以做到定位网页元素、实时监控网页元素属性变化的功能,可以及时调试、修改、定位、追踪检查、查看嵌套,修改样式和查看js动态输出信息。这让我想起了自己当初就是这样直接修改四级成绩,然后骗朋友的,不知道的人还真的想不出这原因,就以为的确是真的啦哈哈。
另一方面,查看源只是把网页输出的源代码,即就是别人服务器发送到浏览器的原封不动的代码直接打开,既不能动态变化,也不能修改。

为了解决这个问题,追求美观,我就想到可以把时钟和标签、分类等菜单中的索引一起,在竖屏状态下不点击时就不显示。在分析了header.swig中菜单部分的源码之后,我注意到一个标签<nav>,它是是HTML5的新标签,可以标注一个导航链接的区域。于是,我将插入时钟的语句移入nav所包裹的块中,就完美达到了我的需求。


意外的问题

在我写这篇博文的时候,出现了一个奇怪的问题。每当我想要本地预览(部署应该也会出现这个问题),都会报错:“Nunjucks Error: [Line 17, Column 239] unexpected token: }”,这就让我非常的苦恼。
根据错误信息,我开始一个一个寻找我文中的花括号。在反复删减和搜索相关问题之后,我发现是我插入在行间的一个include的swig语句惹的祸(我要是写出来又报错,插入在段间就没问题,可以到上文找)。
这类异常一般是文章中使用了大括号{}这个特殊字符,且没有转义导致编译不通过,解决的办法是使用&#123; &#125;对大的花括号进行转换。

补充:小的圆括号可用&#40; &#41;进行转换。

没有这类问题当然再好不过啦,如果出现了,可以试试上面的方法。这类涉及转义的符号还是得熟悉其规则,避免老是出错。

]]>
+

最近心血来潮花了好久给自己的博客添加了一个粒子时钟,最后想要使它在sidebar中居中显示废了我好大功夫,为了以后不在这上面浪费时间,我决定浪费现在的时间把这个问题记下来。

References

电子文献:
https://www.jianshu.com/p/f0bffc42c1ce
https://blog.csdn.net/chwshuang/article/details/52350559


swig

由于我要将我的时钟显示在侧边栏,需要插入到header.swig文件中。hexo博客的源码中有大量这个格式的文件,然而具体的使用方法我也不是很清楚。在查阅了一些文章之后,我对swig有了一个初步的认知。
swig是一个JS前端模板引擎,它有如下特点:

  • 支持大多数主流浏览器。
  • 表达式兼容性好。
  • 面向对象的模板继承。
  • 将过滤器和转换应用到模板中的输出。
  • 可根据路劲渲染页面。
  • 支持页面复用。
  • 支持动态页面。
  • 可扩展、可定制。

可以通过VSchart将swig与其它前端模板框架进行对比,这个网站由维基支持,可以在里面进行各种对比,非常有意思,在这里推荐一下。
关于swig的基本用法,可以在我文首的第一个参考链接中找到,个人认为不搞前端的话大概率是用不到的。


原本的方法

当我添加完时钟本地测验时,我发现添加的时钟在侧边栏的位置没有居中。

根据之前学习html的记忆,我尝试了使用<p align=center></p>标签包裹我的插入语句,然而并没有达到想要的效果。


使用div实现居中

为了达到上述的目的,我使用<div>标签来分割出块,并使用div的属性来实现居中显示的效果。

1
2
3
<div style="Text-align:center;width:100%;">
{% include '../_custom/clock.swig' %}
</div>


使用nav实现隐藏

本以为大功告成,结果在移动端查看时,发现竖屏显示效果非常煞风景。

其实在电脑端浏览器也可以预览移动端的效果,方法很简单,就是直接将浏览器窗口小化,减小两边间距,网页就会自动变成竖屏显示的状态(除非没有)。此外,当我们F12检查元素或者查看源时,网页也会被挤到一侧从而变成竖屏显示的状态。

注:这里补充一下检查元素和查看源之间的区别,一般的浏览器右键都会有这两个功能,表面上看起来似乎也差不多,但是它们还是有区别的。
检查元素看的是渲染过的最终代码,可以做到定位网页元素、实时监控网页元素属性变化的功能,可以及时调试、修改、定位、追踪检查、查看嵌套,修改样式和查看js动态输出信息。这让我想起了自己当初就是这样直接修改四级成绩,然后骗朋友的,不知道的人还真的想不出这原因,就以为的确是真的啦哈哈。
另一方面,查看源只是把网页输出的源代码,即就是别人服务器发送到浏览器的原封不动的代码直接打开,既不能动态变化,也不能修改。

为了解决这个问题,追求美观,我就想到可以把时钟和标签、分类等菜单中的索引一起,在竖屏状态下不点击时就不显示。在分析了header.swig中菜单部分的源码之后,我注意到一个标签<nav>,它是是HTML5的新标签,可以标注一个导航链接的区域。于是,我将插入时钟的语句移入nav所包裹的块中,就完美达到了我的需求。


意外的问题

在我写这篇博文的时候,出现了一个奇怪的问题。每当我想要本地预览(部署应该也会出现这个问题),都会报错:“Nunjucks Error: [Line 17, Column 239] unexpected token: }”,这就让我非常的苦恼。
根据错误信息,我开始一个一个寻找我文中的花括号。在反复删减和搜索相关问题之后,我发现是我插入在行间的一个include的swig语句惹的祸(我要是写出来又报错,插入在段间就没问题,可以到上文找)。
这类异常一般是文章中使用了大括号{}这个特殊字符,且没有转义导致编译不通过,解决的办法是使用&#123; &#125;对大的花括号进行转换。

补充:小的圆括号可用&#40; &#41;进行转换。

没有这类问题当然再好不过啦,如果出现了,可以试试上面的方法。这类涉及转义的符号还是得熟悉其规则,避免老是出错。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>最近心血来潮花了好久给自己的博客添加了一个粒子时钟,最后想要使它在sidebar中居中显示废了我好大功夫,为了以后不在这上面浪费时间,我决定浪费 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>最近心血来潮花了好久给自己的博客添加了一个粒子时钟,最后想要使它在sidebar中居中显示废了我好大功夫,为了以后不在这上面浪费时间,我决定浪费 @@ -1625,13 +1625,13 @@ 2019-11-02T03:24:35.000Z 2020-02-07T09:22:58.339Z -

kaggle是一个著名的数据科学竞赛平台,暑假里我也抽空自己独立完成了三四个getting started级别的比赛。对于MNIST数据集,想必入门计算机视觉的人应该也不会陌生。kaggle上getting started的第一个比赛就是Digit Recognizer:Learn computer vision fundamentals with the famous MNIST data。当时作为入门小白的我,使用了入门级的方法KNN完成了我的第一次机器学习(自认为KNN是最最基础的算法,对它的介绍可见我的另一篇博文machine-learning笔记:机器学习的几个常见算法及其优缺点,真的非常简单,但也极其笨拙)。而最近我又使用CNN再一次尝试了这个数据集,踩了不少坑,因此想把两次经历统统记录在这,可能会有一些不足之处,留作以后整理优化。

References

电子文献:
https://blog.csdn.net/gybinlero/article/details/79294649
https://blog.csdn.net/qq_43497702/article/details/95005248
https://blog.csdn.net/a19990412/article/details/90349429


KNN

首先导入必要的包,这里基本用不到太多:

1
2
3
4
5
6
7
import numpy as np
import csv
import operator

import matplotlib
from matplotlib import pyplot as plt
%matplotlib inline

导入训练数据:

1
2
3
4
5
6
7
8
9
10
trainSet = []
with open('train.csv','r') as trainFile:
lines=csv.reader(trainFile)
for line in lines:
trainSet.append(line)
trainSet.remove(trainSet[0])

trainSet = np.array(trainSet)
rawTrainLabel = trainSet[:, 0] #分割出训练集标签
rawTrainData = trainSet[:, 1:] #分割出训练集数据

我当时用了一种比较笨拙的办法转换数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
rawTrainData = np.mat(rawTrainData) #转化成矩阵,或许不需要
m, n = np.shape(rawTrainData)
trainData = np.zeros((m, n)) #创建初值为0的ndarray
for i in range(m):
for j in range(n):
trainData[i, j] = int(rawTrainData[i, j]) #转化并赋值

rawTrainLabel = np.mat(rawTrainLabel) #或许不需要
m, n = np.shape(rawTrainLabel)
trainLabel = np.zeros((m, n))
for i in range(m):
for j in range(n):
trainLabel[i, j] = int(rawTrainLabel[i, j])

这里我们可以查看以下数据的维度,确保没有出错。

为了方便起见,我们把所有pixel不为0的点都设置为1。

1
2
3
4
5
m, n = np.shape(trainData)
for i in range(m):
for j in range(n):
if trainData[i, j] != 0:
trainData[i, j] = 1

仿照训练集的步骤,导入测试集并做相同处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
testSet = []
with open('test.csv','r') as testFile:
lines=csv.reader(testFile)
for line in lines:
testSet.append(line)
testSet.remove(testSet[0])

testSet = np.array(testSet)
rawTestData = testSet

rawTestData = np.mat(rawTestData)
m, n = np.shape(rawTestData)
testData = np.zeros((m, n))
for i in range(m):
for j in range(n):
testData[i, j] = int(rawTestData[i, j])

m, n = np.shape(testData)
for i in range(m):
for j in range(n):
if testData[i, j] != 0:
testData[i, j] = 1

同样的,可使用testData.shape查看测试集的维度,保证它是28000*784,由此可知操作无误。
接下来,我们定义KNN的分类函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def classify(inX, dataSet, labels, k):
inX = np.mat(inX)
dataSet = np.mat(dataSet)
labels = np.mat(labels)
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = np.array(diffMat) ** 2
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances ** 0.5
sortedDistIndicies = distances.argsort()
classCount={}
for i in range(k):
voteIlabel = labels[0, sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse = True)
return sortedClassCount[0][0]

为了更好地分类,这里我们需要选择合适的k值,我选取了4000个样本作为验证机进行尝试,找到误差最小的k值并作为最终的k值输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
trainingTestSize = 4000

#分割出验证集
m, n = np.shape(trainLabel)
trainingTrainLabel = np.zeros((m, n - trainingTestSize))
for i in range(m):
for j in range(n - trainingTestSize):
trainingTrainLabel[i, j] = trainLabel[i, j]

trainingTestLabel = np.zeros((m, trainingTestSize))
for i in range(m):
for j in range(trainingTestSize):
trainingTestLabel[i, j] = trainLabel[i, n - trainingTestSize + j]

m, n = np.shape(trainData)
trainingTrainData = np.zeros((m - trainingTestSize, n))
for i in range(m - trainingTestSize):
for j in range(n):
trainingTrainData[i, j] = trainData[i, j]

trainingTestData = np.zeros((trainingTestSize, n))
for i in range(trainingTestSize):
for j in range(n):
trainingTestData[i, j] = trainData[m - trainingTestSize + i, j]

#使k值为3到9依次尝试
training = []
for x in range(3, 10):
error = 0
for y in range(trainingTestSize):
answer = (classify(trainingTestData[y], trainingTrainData, trainingTrainLabel, x))
print 'the classifier came back with: %d, %.2f%% has done, the k now is %d' % (answer, (y + (x - 3) * trainingTestSize) / float(trainingTestSize * 7) * 100, x) #方便知道进度
if answer != trainingTestLabel[0, y]:
error += 1
training.append(error)

这个过程比较长,结果会得到training的结果是[156, 173, 159, 164, 152, 155, 156]。
可以使用plt.plot(training)更直观地查看误差,呈现如下:

注意:这里的下标应该加上3才是对应的k值。

可以看图手动选择k值,但由于事先无法把握训练结束的时间,可以编写函数自动选择并使程序继续进行。

1
2
3
4
5
6
theK = 3
hasError = training[0]
for i in range(7):
if training[i] < hasError:
theK = i + 3
hasError = training[i]

在确定k值后,接下来就是代入测试集进行结果的计算了。由于KNN算法相对而言比较低级,因此就别指望效率了,跑CPU的话整个过程大概需要半天左右。

1
2
3
4
5
6
m, n = np.shape(testData)
result = []
for i in range(m):
answer = (classify(testData[i], trainData, trainLabel, theK))
result.append(answer)
print 'the classifier came back with: %d, %.2f%% has done' % (answer, i / float(m) * 100)

最后,定义一个保存结果的函数,然后saveResult(result)之后,再对csv文件进行处理(后文会提到),然后就可以submit了。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最终此方法在kaggle上获得的score为0.96314,准确率还是挺高的,主要是因为问题相对简单,放到leaderboard上,这结果的排名就要到两千左右了。


CNN

在学习了卷积神经网络和pytorch框架之后,我决定使用CNN对这个比赛再进行一次尝试。
首先还是导入相关的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import pandas as pd
import numpy as np
from math import *

%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.cm as cm

import torch.utils.data as Data

from torch.autograd import Variable

import csv

导入训练数据,可以使用train.head()查看导入的结果,便于后续的处理。

1
train = pd.read_csv("train.csv")

对数据进行处理,由于要使用的是CNN,我们必须要把数据整理成能输入的形式,即从数组变成高维张量。

1
2
3
4
5
train_labels = torch.from_numpy(np.array(train.label[:]))

image_size = train.iloc[:, 1:].shape[1]
image_width = image_height = np.ceil(np.sqrt(image_size)).astype(np.uint8)
train_data = torch.FloatTensor(np.array(train.iloc[:, 1:]).reshape((-1, 1, image_width, image_height))) / 255 #灰度压缩,进行归一化

注:reshape中的-1表示自适应,这样我们能让我们更好的变化数据的形式。

我们可以使用matplotlib查看数据处理的结果。

1
2
3
plt.imshow(train_data[1].numpy().squeeze(), cmap = 'gray')
plt.title('%i' % train_labels[1])
plt.show()

可以看到如下图片,可以与plt.title进行核对。

注:可以用squeeze()函数来降维,例如:从[[1]]—>[1]
与之相反的是便是unsqueeze(dim = 1),该函数可以使[1]—>[[1]]

以同样的方式导入并处理测试集。

1
2
test= pd.read_csv("test.csv")
test_data = torch.FloatTensor(np.array(test).reshape((-1, 1, image_width, image_height))) / 255

接下来我们定义几个超参数,这里将要使用的是小批梯度下降的优化算法,因此定义如下:

1
2
3
4
#超参数
EPOCH = 1 #整个数据集循环训练的轮数
BATCH_SIZE = 10 #每批的样本个数
LR = 0.01 #学习率

定义好超参数之后,我们使用Data对数据进行最后的处理。

1
2
3
4
5
6
7
trainData = Data.TensorDataset(train_data, train_labels) #用后会变成元组类型

train_loader = Data.DataLoader(
dataset = trainData,
batch_size = BATCH_SIZE,
shuffle = True
)

上面的Data.TensorDataset可以把数据进行打包,以方便我们更好的使用;而Data.DataLoade可以将我们的数据打乱并且分批。要注意的是,这里不要对测试集进行操作,否则最终输出的结果就难以再与原来的顺序匹配了。
接下来,我们定义卷积神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#build CNN
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
#一个卷积层
self.conv1 = nn.Sequential(
nn.Conv2d( #输入(1, 28, 28)
in_channels = 1, #1个通道
out_channels = 16, #输出层数
kernel_size = 5, #过滤器的大小
stride = 1, #步长
padding = 2 #填白
), #输出(16, 28, 28)
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
)
self.conv2 = nn.Sequential( #输入(16, 14, 14)
nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
)
self.out = nn.Linear(32 * 7 * 7, 10) #全连接层,将三维的数据展为2维的数据并输出

def forward(self, x): #父类已定义,不能修改名字
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
output = F.softmax(self.out(x))
return output

cnn = CNN()
optimzer = torch.optim.Adam(cnn.parameters(), lr = LR) #define optimezer
loss_func = nn.CrossEntropyLoss() #loss function使用交叉嫡误差

print(cnn) # 查看net architecture

完成以上的操作之后,就可以开始训练了,整个训练时间在CPU上只需要几分钟,这比KNN算法要优越许多。

1
2
3
4
5
6
7
8
9
10
11
12
for epoch in range(EPOCH):
for step, (x, y) in enumerate(train_loader):
b_x = Variable(x)
b_y = Variable(y)
output = cnn(b_x)
loss = loss_func(output, b_y) #cross entropy loss
#update W
optimzer.zero_grad()
loss.backward()
optimzer.step()
print('epoch%d' % (epoch + 1), '-', 'batch%d' % step, '-', 'loss%f' % loss) #查看训练过程
print('No.%depoch is over' % (epoch + 1))

代入测试集求解:

1
2
3
4
5
output = cnn(test_data[:])
#print(output)

result = torch.max(output, 1)[1].squeeze()
#print(result)

仿照KNN中的结果转存函数,定义saveResult函数。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最后使用saveResult(result.numpy())把结果存入csv文件。


改进

然而,若使用上述的CNN,得出的结果在leaderboard上会达到两千三百多名,这已经进入所有参赛者的倒数两百名之内了。为什么这个CNN的表现甚至不如我前面的KNN算法呢?我觉得主要有下面三个原因。

  1. 首先,由于CNN的参数较多,仅经过1轮epoch应该是不足够把所有参数训练到最优或者接近最优的位置的。个人认为,靠前的数据在参数相对远离最优值时参与训练而在之后不起作用,很有可能导致最后顾此失彼,因此有必要增加epoch使之前的数据多次参与参数的校正。同时,也要增大batch size使每次优化参数使用的样本更多,从而在测试集上表现更好。训练结束后,我发现我的C盘会被占用几个G,不知道是不是出错了,也有可能是参数占用的空间,必须停止kernel才能得到释放(我关闭了VScode后刷新,空间就回来了)。关于内存,这里似乎存在着一个问题,我将在后文阐述。

    注:由于VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试。

  2. 学习率过大。尽管我这里的学习率设置为0.01,但对于最后的收敛来说或许还是偏大,这就导致了最后会在最优解附近来回抖动而难以接近的问题。关于这个问题,可以到deep-learning笔记:学习率衰减与批归一化中看看我较为详细的分析与解决方法。

  3. 由于训练时间和epoch轮数相对较小,我推测模型可能会存在过拟合的问题。尤其是最后的全连接层,它的结构很容易造成过拟合。关于这个问题,也可以到machine-learning笔记:过拟合与欠拟合machine-learning笔记:机器学习中正则化的理解中看看我较为详细的分析与解决方法。

针对上述原因,我对我的CNN模型做了如下调整:

  1. 首先,增加训练量,调整超参数如下。

    1
    2
    3
    4
    #超参数
    EOPCH = 3
    BATCH_SIZE = 50
    LR = 1e-4
  2. 引入dropout随机失活,加强全连接层的鲁棒性,修改网络结构如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #build CNN
    class CNN(nn.Module):
    def __init__(self):
    super(CNN, self).__init__()
    #一个卷积层
    self.conv1 = nn.Sequential(
    nn.Conv2d( #输入(1, 28, 28)
    in_channels = 1, #1个通道
    out_channels = 16, #输出层数
    kernel_size = 5, #过滤器的大小
    stride = 1, #步长
    padding = 2 #填白
    ), #输出(16, 28, 28)
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
    )
    self.conv2 = nn.Sequential( #输入(16, 14, 14)
    nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
    )

    self.dropout = nn.Dropout(p = 0.5) #每次减少50%神经元之间的连接

    self.fc = nn.Linear(32 * 7 * 7, 1024)
    self.out = nn.Linear(1024, 10) #全连接层,将三维的数据展为2维的数据并输出

    def forward(self, x):
    x = self.conv1(x)
    x = self.conv2(x)
    x = x.view(x.size(0), -1)
    x = self.fc(x)
    x = self.dropout(x)
    output = F.softmax(self.out(x))
    return output

本想直接使用torch.nn.functional中的dropout函数轻松实现随机失活正则化,但在网上看到这个函数好像有点坑,因此就不以身试坑了,还是在网络初始化中先定义dropout。

注:训练完新定义的网络之后我一直在思考dropout添加的方式与位置。在看了一些资料之后,我认为或许去掉全连接层、保持原来的层数并在softmax之前dropout可能能达到更好的效果。考虑到知乎上有知友提到做研究试验不宜在MNIST这些玩具级别的数据集上进行,因此暂时不再做没有太大意义的调整,今后有空在做改进试验。

经过上面的改进后,我再次训练网络并提交结果,在kaggle上的评分提高至0.97328,大约处在1600名左右,可以继续调整超参数(可以分割验证集寻找)和加深网络结构以取得更高的分数,但我的目的已经达到了。与之前的KNN相比,无论从时间效率还是准确率,CNN都有很大的进步,这也体现了深度学习相对于一些经典机器学习算法的优势。


出现的问题

由于这个最后的网络是我重复构建之后完成的,因此下列部分问题可能不存在于我上面的代码中,但我还是想汇总在这,以防之后继续踩相同的坑。

  1. 报错element 0 of tensors does not require grad and does not have a grad_fn

    pytorch具有自动求导机制,这就省去了我们编写反向传播的代码。每个Variable变量都有两个标志:requires_grad和volatile。出现上述问题的原因是requires_grad = False,修改或者增加(因为默认是false)成True即可。
  2. RuntimeError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

    这个好像是我在计算交叉熵时遇到的,原因是因为torch的交叉熵的输入第一个位置的输入应该是在每个label下的概率,而不是对应的label,详细分析与举例可参考文首给出的第三个链接。
  3. AttributeError: ‘tuple’ object has no attribute ‘numpy’

    为了查看数据处理效果,我在数据预处理过程中使用matplotlib绘制出处理后的图像,但是却出现了如上报错,当时的代码如下:

    1
    2
    3
    plt.imshow(trainData[1].numpy().squeeze(), cmap = 'gray')
    plt.title('%i' % train_labels[1])
    plt.show()

    查找相关资料之后,我才知道torch.utils.data会把打包的数据变成元组类型,因此我们绘图还是要使用原来train_data中的数据。

  4. 转存结果时提醒DefaultCPUAllocator: not enough memory

    由于当初在实现KNN算法转存结果时使用的函数存入csv文件后还要对文件进行空值删除处理,比较麻烦(后文会写具体如何处理),因此我想借用文章顶部给出的第二个链接中提供的方法:

    1
    2
    3
    out = pd.DataFrame(np.array(result), index = range(1, 1 + len(result)), columns = ['ImageId', 'Label'])
    #torch和pandas的类型不能直接的转换,所以需要借助numpy中间的步骤,将torch的数据转给pandas
    out.to_csv('result.csv', header = None)

    结果出现如下错误:

    我好歹也是八千多买的DELL旗舰本,8G内存,它居然说我不够让我换块新的RAM?什么情况…
    尝试许久,我怀疑是训练得到的参数占用了我的内存,那只好先把训练出的result保存下来,再导入到csv文件。
    最后我还是选择自己手动处理csv文件中的空值,应该有其它的转存csv文件的方法或者上述问题的解决措施,留待以后实践过程中发现解决,也欢迎大家不吝赐教。


excel/csv快速删除空白行

如果你使用的是我的saveResult函数或者类似,你就很有可能发现更新后的csv文件中数据之间双数行都是留空的,即一列数据之间都有空白行相隔,那么可以使用如下方法快速删除空白行。

  1. 选中对应列或者区域。
  2. 在“开始”工具栏中找到“查找与选择”功能并点击。
  3. 在下拉菜单中,点击“定位条件”选项。
  4. 在打开的定位条件窗口中,选择“空值”并确定。
  5. 待电脑为你选中所有空值后,任意右键一个被选中的空白行,在弹出的菜单中点击“删除”。
  6. 如果数据量比较大,这时候会有一个处理时间可能会比较长的提醒弹出,确认即可。
  7. 等待处理完毕。
]]>
+

kaggle是一个著名的数据科学竞赛平台,暑假里我也抽空自己独立完成了三四个getting started级别的比赛。对于MNIST数据集,想必入门计算机视觉的人应该也不会陌生。kaggle上getting started的第一个比赛就是Digit Recognizer:Learn computer vision fundamentals with the famous MNIST data。当时作为入门小白的我,使用了入门级的方法KNN完成了我的第一次机器学习(自认为KNN是最最基础的算法,对它的介绍可见我的另一篇博文machine-learning笔记:机器学习的几个常见算法及其优缺点,真的非常简单,但也极其笨拙)。而最近我又使用CNN再一次尝试了这个数据集,踩了不少坑,因此想把两次经历统统记录在这,可能会有一些不足之处,留作以后整理优化。

References

电子文献:
https://blog.csdn.net/gybinlero/article/details/79294649
https://blog.csdn.net/qq_43497702/article/details/95005248
https://blog.csdn.net/a19990412/article/details/90349429


KNN

首先导入必要的包,这里基本用不到太多:

1
2
3
4
5
6
7
import numpy as np
import csv
import operator

import matplotlib
from matplotlib import pyplot as plt
%matplotlib inline

导入训练数据:

1
2
3
4
5
6
7
8
9
10
trainSet = []
with open('train.csv','r') as trainFile:
lines=csv.reader(trainFile)
for line in lines:
trainSet.append(line)
trainSet.remove(trainSet[0])

trainSet = np.array(trainSet)
rawTrainLabel = trainSet[:, 0] #分割出训练集标签
rawTrainData = trainSet[:, 1:] #分割出训练集数据

我当时用了一种比较笨拙的办法转换数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
rawTrainData = np.mat(rawTrainData) #转化成矩阵,或许不需要
m, n = np.shape(rawTrainData)
trainData = np.zeros((m, n)) #创建初值为0的ndarray
for i in range(m):
for j in range(n):
trainData[i, j] = int(rawTrainData[i, j]) #转化并赋值

rawTrainLabel = np.mat(rawTrainLabel) #或许不需要
m, n = np.shape(rawTrainLabel)
trainLabel = np.zeros((m, n))
for i in range(m):
for j in range(n):
trainLabel[i, j] = int(rawTrainLabel[i, j])

这里我们可以查看以下数据的维度,确保没有出错。

为了方便起见,我们把所有pixel不为0的点都设置为1。

1
2
3
4
5
m, n = np.shape(trainData)
for i in range(m):
for j in range(n):
if trainData[i, j] != 0:
trainData[i, j] = 1

仿照训练集的步骤,导入测试集并做相同处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
testSet = []
with open('test.csv','r') as testFile:
lines=csv.reader(testFile)
for line in lines:
testSet.append(line)
testSet.remove(testSet[0])

testSet = np.array(testSet)
rawTestData = testSet

rawTestData = np.mat(rawTestData)
m, n = np.shape(rawTestData)
testData = np.zeros((m, n))
for i in range(m):
for j in range(n):
testData[i, j] = int(rawTestData[i, j])

m, n = np.shape(testData)
for i in range(m):
for j in range(n):
if testData[i, j] != 0:
testData[i, j] = 1

同样的,可使用testData.shape查看测试集的维度,保证它是28000*784,由此可知操作无误。
接下来,我们定义KNN的分类函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def classify(inX, dataSet, labels, k):
inX = np.mat(inX)
dataSet = np.mat(dataSet)
labels = np.mat(labels)
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = np.array(diffMat) ** 2
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances ** 0.5
sortedDistIndicies = distances.argsort()
classCount={}
for i in range(k):
voteIlabel = labels[0, sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse = True)
return sortedClassCount[0][0]

为了更好地分类,这里我们需要选择合适的k值,我选取了4000个样本作为验证机进行尝试,找到误差最小的k值并作为最终的k值输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
trainingTestSize = 4000

#分割出验证集
m, n = np.shape(trainLabel)
trainingTrainLabel = np.zeros((m, n - trainingTestSize))
for i in range(m):
for j in range(n - trainingTestSize):
trainingTrainLabel[i, j] = trainLabel[i, j]

trainingTestLabel = np.zeros((m, trainingTestSize))
for i in range(m):
for j in range(trainingTestSize):
trainingTestLabel[i, j] = trainLabel[i, n - trainingTestSize + j]

m, n = np.shape(trainData)
trainingTrainData = np.zeros((m - trainingTestSize, n))
for i in range(m - trainingTestSize):
for j in range(n):
trainingTrainData[i, j] = trainData[i, j]

trainingTestData = np.zeros((trainingTestSize, n))
for i in range(trainingTestSize):
for j in range(n):
trainingTestData[i, j] = trainData[m - trainingTestSize + i, j]

#使k值为3到9依次尝试
training = []
for x in range(3, 10):
error = 0
for y in range(trainingTestSize):
answer = (classify(trainingTestData[y], trainingTrainData, trainingTrainLabel, x))
print 'the classifier came back with: %d, %.2f%% has done, the k now is %d' % (answer, (y + (x - 3) * trainingTestSize) / float(trainingTestSize * 7) * 100, x) #方便知道进度
if answer != trainingTestLabel[0, y]:
error += 1
training.append(error)

这个过程比较长,结果会得到training的结果是[156, 173, 159, 164, 152, 155, 156]。
可以使用plt.plot(training)更直观地查看误差,呈现如下:

注意:这里的下标应该加上3才是对应的k值。

可以看图手动选择k值,但由于事先无法把握训练结束的时间,可以编写函数自动选择并使程序继续进行。

1
2
3
4
5
6
theK = 3
hasError = training[0]
for i in range(7):
if training[i] < hasError:
theK = i + 3
hasError = training[i]

在确定k值后,接下来就是代入测试集进行结果的计算了。由于KNN算法相对而言比较低级,因此就别指望效率了,跑CPU的话整个过程大概需要半天左右。

1
2
3
4
5
6
m, n = np.shape(testData)
result = []
for i in range(m):
answer = (classify(testData[i], trainData, trainLabel, theK))
result.append(answer)
print 'the classifier came back with: %d, %.2f%% has done' % (answer, i / float(m) * 100)

最后,定义一个保存结果的函数,然后saveResult(result)之后,再对csv文件进行处理(后文会提到),然后就可以submit了。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最终此方法在kaggle上获得的score为0.96314,准确率还是挺高的,主要是因为问题相对简单,放到leaderboard上,这结果的排名就要到两千左右了。


CNN

在学习了卷积神经网络和pytorch框架之后,我决定使用CNN对这个比赛再进行一次尝试。
首先还是导入相关的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import pandas as pd
import numpy as np
from math import *

%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.cm as cm

import torch.utils.data as Data

from torch.autograd import Variable

import csv

导入训练数据,可以使用train.head()查看导入的结果,便于后续的处理。

1
train = pd.read_csv("train.csv")

对数据进行处理,由于要使用的是CNN,我们必须要把数据整理成能输入的形式,即从数组变成高维张量。

1
2
3
4
5
train_labels = torch.from_numpy(np.array(train.label[:]))

image_size = train.iloc[:, 1:].shape[1]
image_width = image_height = np.ceil(np.sqrt(image_size)).astype(np.uint8)
train_data = torch.FloatTensor(np.array(train.iloc[:, 1:]).reshape((-1, 1, image_width, image_height))) / 255 #灰度压缩,进行归一化

注:reshape中的-1表示自适应,这样我们能让我们更好的变化数据的形式。

我们可以使用matplotlib查看数据处理的结果。

1
2
3
plt.imshow(train_data[1].numpy().squeeze(), cmap = 'gray')
plt.title('%i' % train_labels[1])
plt.show()

可以看到如下图片,可以与plt.title进行核对。

注:可以用squeeze()函数来降维,例如:从[[1]]—>[1]
与之相反的是便是unsqueeze(dim = 1),该函数可以使[1]—>[[1]]

以同样的方式导入并处理测试集。

1
2
test= pd.read_csv("test.csv")
test_data = torch.FloatTensor(np.array(test).reshape((-1, 1, image_width, image_height))) / 255

接下来我们定义几个超参数,这里将要使用的是小批梯度下降的优化算法,因此定义如下:

1
2
3
4
#超参数
EPOCH = 1 #整个数据集循环训练的轮数
BATCH_SIZE = 10 #每批的样本个数
LR = 0.01 #学习率

定义好超参数之后,我们使用Data对数据进行最后的处理。

1
2
3
4
5
6
7
trainData = Data.TensorDataset(train_data, train_labels) #用后会变成元组类型

train_loader = Data.DataLoader(
dataset = trainData,
batch_size = BATCH_SIZE,
shuffle = True
)

上面的Data.TensorDataset可以把数据进行打包,以方便我们更好的使用;而Data.DataLoade可以将我们的数据打乱并且分批。要注意的是,这里不要对测试集进行操作,否则最终输出的结果就难以再与原来的顺序匹配了。
接下来,我们定义卷积神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#build CNN
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
#一个卷积层
self.conv1 = nn.Sequential(
nn.Conv2d( #输入(1, 28, 28)
in_channels = 1, #1个通道
out_channels = 16, #输出层数
kernel_size = 5, #过滤器的大小
stride = 1, #步长
padding = 2 #填白
), #输出(16, 28, 28)
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
)
self.conv2 = nn.Sequential( #输入(16, 14, 14)
nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
)
self.out = nn.Linear(32 * 7 * 7, 10) #全连接层,将三维的数据展为2维的数据并输出

def forward(self, x): #父类已定义,不能修改名字
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
output = F.softmax(self.out(x))
return output

cnn = CNN()
optimzer = torch.optim.Adam(cnn.parameters(), lr = LR) #define optimezer
loss_func = nn.CrossEntropyLoss() #loss function使用交叉嫡误差

print(cnn) # 查看net architecture

完成以上的操作之后,就可以开始训练了,整个训练时间在CPU上只需要几分钟,这比KNN算法要优越许多。

1
2
3
4
5
6
7
8
9
10
11
12
for epoch in range(EPOCH):
for step, (x, y) in enumerate(train_loader):
b_x = Variable(x)
b_y = Variable(y)
output = cnn(b_x)
loss = loss_func(output, b_y) #cross entropy loss
#update W
optimzer.zero_grad()
loss.backward()
optimzer.step()
print('epoch%d' % (epoch + 1), '-', 'batch%d' % step, '-', 'loss%f' % loss) #查看训练过程
print('No.%depoch is over' % (epoch + 1))

代入测试集求解:

1
2
3
4
5
output = cnn(test_data[:])
#print(output)

result = torch.max(output, 1)[1].squeeze()
#print(result)

仿照KNN中的结果转存函数,定义saveResult函数。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最后使用saveResult(result.numpy())把结果存入csv文件。


改进

然而,若使用上述的CNN,得出的结果在leaderboard上会达到两千三百多名,这已经进入所有参赛者的倒数两百名之内了。为什么这个CNN的表现甚至不如我前面的KNN算法呢?我觉得主要有下面三个原因。

  1. 首先,由于CNN的参数较多,仅经过1轮epoch应该是不足够把所有参数训练到最优或者接近最优的位置的。个人认为,靠前的数据在参数相对远离最优值时参与训练而在之后不起作用,很有可能导致最后顾此失彼,因此有必要增加epoch使之前的数据多次参与参数的校正。同时,也要增大batch size使每次优化参数使用的样本更多,从而在测试集上表现更好。训练结束后,我发现我的C盘会被占用几个G,不知道是不是出错了,也有可能是参数占用的空间,必须停止kernel才能得到释放(我关闭了VScode后刷新,空间就回来了)。关于内存,这里似乎存在着一个问题,我将在后文阐述。

    注:由于VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试。

  2. 学习率过大。尽管我这里的学习率设置为0.01,但对于最后的收敛来说或许还是偏大,这就导致了最后会在最优解附近来回抖动而难以接近的问题。关于这个问题,可以到deep-learning笔记:学习率衰减与批归一化中看看我较为详细的分析与解决方法。

  3. 由于训练时间和epoch轮数相对较小,我推测模型可能会存在过拟合的问题。尤其是最后的全连接层,它的结构很容易造成过拟合。关于这个问题,也可以到machine-learning笔记:过拟合与欠拟合machine-learning笔记:机器学习中正则化的理解中看看我较为详细的分析与解决方法。

针对上述原因,我对我的CNN模型做了如下调整:

  1. 首先,增加训练量,调整超参数如下。

    1
    2
    3
    4
    #超参数
    EOPCH = 3
    BATCH_SIZE = 50
    LR = 1e-4
  2. 引入dropout随机失活,加强全连接层的鲁棒性,修改网络结构如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #build CNN
    class CNN(nn.Module):
    def __init__(self):
    super(CNN, self).__init__()
    #一个卷积层
    self.conv1 = nn.Sequential(
    nn.Conv2d( #输入(1, 28, 28)
    in_channels = 1, #1个通道
    out_channels = 16, #输出层数
    kernel_size = 5, #过滤器的大小
    stride = 1, #步长
    padding = 2 #填白
    ), #输出(16, 28, 28)
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
    )
    self.conv2 = nn.Sequential( #输入(16, 14, 14)
    nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
    )

    self.dropout = nn.Dropout(p = 0.5) #每次减少50%神经元之间的连接

    self.fc = nn.Linear(32 * 7 * 7, 1024)
    self.out = nn.Linear(1024, 10) #全连接层,将三维的数据展为2维的数据并输出

    def forward(self, x):
    x = self.conv1(x)
    x = self.conv2(x)
    x = x.view(x.size(0), -1)
    x = self.fc(x)
    x = self.dropout(x)
    output = F.softmax(self.out(x))
    return output

本想直接使用torch.nn.functional中的dropout函数轻松实现随机失活正则化,但在网上看到这个函数好像有点坑,因此就不以身试坑了,还是在网络初始化中先定义dropout。

注:训练完新定义的网络之后我一直在思考dropout添加的方式与位置。在看了一些资料之后,我认为或许去掉全连接层、保持原来的层数并在softmax之前dropout可能能达到更好的效果。考虑到知乎上有知友提到做研究试验不宜在MNIST这些玩具级别的数据集上进行,因此暂时不再做没有太大意义的调整,今后有空在做改进试验。

经过上面的改进后,我再次训练网络并提交结果,在kaggle上的评分提高至0.97328,大约处在1600名左右,可以继续调整超参数(可以分割验证集寻找)和加深网络结构以取得更高的分数,但我的目的已经达到了。与之前的KNN相比,无论从时间效率还是准确率,CNN都有很大的进步,这也体现了深度学习相对于一些经典机器学习算法的优势。


出现的问题

由于这个最后的网络是我重复构建之后完成的,因此下列部分问题可能不存在于我上面的代码中,但我还是想汇总在这,以防之后继续踩相同的坑。

  1. 报错element 0 of tensors does not require grad and does not have a grad_fn

    pytorch具有自动求导机制,这就省去了我们编写反向传播的代码。每个Variable变量都有两个标志:requires_grad和volatile。出现上述问题的原因是requires_grad = False,修改或者增加(因为默认是false)成True即可。
  2. RuntimeError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

    这个好像是我在计算交叉熵时遇到的,原因是因为torch的交叉熵的输入第一个位置的输入应该是在每个label下的概率,而不是对应的label,详细分析与举例可参考文首给出的第三个链接。
  3. AttributeError: ‘tuple’ object has no attribute ‘numpy’

    为了查看数据处理效果,我在数据预处理过程中使用matplotlib绘制出处理后的图像,但是却出现了如上报错,当时的代码如下:

    1
    2
    3
    plt.imshow(trainData[1].numpy().squeeze(), cmap = 'gray')
    plt.title('%i' % train_labels[1])
    plt.show()

    查找相关资料之后,我才知道torch.utils.data会把打包的数据变成元组类型,因此我们绘图还是要使用原来train_data中的数据。

  4. 转存结果时提醒DefaultCPUAllocator: not enough memory

    由于当初在实现KNN算法转存结果时使用的函数存入csv文件后还要对文件进行空值删除处理,比较麻烦(后文会写具体如何处理),因此我想借用文章顶部给出的第二个链接中提供的方法:

    1
    2
    3
    out = pd.DataFrame(np.array(result), index = range(1, 1 + len(result)), columns = ['ImageId', 'Label'])
    #torch和pandas的类型不能直接的转换,所以需要借助numpy中间的步骤,将torch的数据转给pandas
    out.to_csv('result.csv', header = None)

    结果出现如下错误:

    我好歹也是八千多买的DELL旗舰本,8G内存,它居然说我不够让我换块新的RAM?什么情况…
    尝试许久,我怀疑是训练得到的参数占用了我的内存,那只好先把训练出的result保存下来,再导入到csv文件。
    最后我还是选择自己手动处理csv文件中的空值,应该有其它的转存csv文件的方法或者上述问题的解决措施,留待以后实践过程中发现解决,也欢迎大家不吝赐教。


excel/csv快速删除空白行

如果你使用的是我的saveResult函数或者类似,你就很有可能发现更新后的csv文件中数据之间双数行都是留空的,即一列数据之间都有空白行相隔,那么可以使用如下方法快速删除空白行。

  1. 选中对应列或者区域。
  2. 在“开始”工具栏中找到“查找与选择”功能并点击。
  3. 在下拉菜单中,点击“定位条件”选项。
  4. 在打开的定位条件窗口中,选择“空值”并确定。
  5. 待电脑为你选中所有空值后,任意右键一个被选中的空白行,在弹出的菜单中点击“删除”。
  6. 如果数据量比较大,这时候会有一个处理时间可能会比较长的提醒弹出,确认即可。
  7. 等待处理完毕。
]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>kaggle是一个著名的数据科学竞赛平台,暑假里我也抽空自己独立完成了三四个getting started级别的比赛。对于MNIST数据集,想必 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>kaggle是一个著名的数据科学竞赛平台,暑假里我也抽空自己独立完成了三四个getting started级别的比赛。对于MNIST数据集,想必 @@ -1640,14 +1640,14 @@ - - - - + + + + @@ -1659,13 +1659,13 @@ 2019-11-01T13:20:14.000Z 2020-02-07T09:19:24.237Z -

toefl笔记:首考托福——记一次裸考经历文章末尾我曾提到在被百度收录之后要好好做SEO,这段时间我也的确有所尝试与改进,因此在本文中将一些我认为比较有效的或者依旧存疑的SEO优化方法写下来,供日后参考深究。

References

电子文献:
https://blog.csdn.net/lzy98/article/details/81140704
https://www.jianshu.com/p/86557c34b671
https://baijiahao.baidu.com/s?id=1616368344109675728&wfr=spider&for=pc
https://www.jianshu.com/p/7e1166eb412a
https://baijiahao.baidu.com/s?id=1597172076743185609&wfr=spider&for=pc


SEO

之前在知乎上碰巧看到一篇别人是如何推广自己的博客的文章,里面就提到了SEO这个概念。我当时也很好奇,百度之后才发现它完全不同于CEO、CTO等概念。SEO(Search Engine Optimization),汉译为搜索引擎优化。它是一种方式,即利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。通俗的讲就是post的内容更容易被搜索引擎搜索到或者收录,且在搜索结果列表中显示靠前。
看了一圈,SEO的办法真的是多种多样,下面我就简单记录一部分我试过的方法。


优化url

同样在站点配置文件下面,可以找到站点的url设置。
如果你尚未更改过,你会发现默认的url是http://yoursite.com,我在这里吃了不少亏,之前苹果上add to favorites、RSS订阅后点开的链接以及copyright的链接都会直接跳转到yoursite而非我的博文链接。
SEO搜索引擎优化认为,网站的最佳结构是用户从首页点击三次就可以到达任何一个页面,但是我们使用hexo编译的站点默认打开文章的url是“sitename/year/mounth/day/title”四层的结构,这样的url结构很不利于SEO,爬虫就会经常爬不到我们的文章,于是,我们可以将url直接改成“sitename/title”的形式,并且title最好是用英文(中文的url会出现好多乱码,我这方面还有待改进)。
基于以上原因,我在根目录的站点配置文件下修改url设置如下(注释中是默认的):

如此,再次添加RSS订阅,就可以跟yoursite这个鬼地方say goodbye啦。
对permalink的修改将会是你的站点的一次巨大的变动,会造成大量的死链。死链会造成搜索引擎对网站的评分降低并有可能会降权。我们可以直接百度搜索“site:url”(url即你的站点网址)查看已经被搜索引擎收录的网址。如下图所示,当时我被收录了四个(截止本文最近更新前收录已达60多),其中前两个经此番调整已成为死链。


这时我们可以在百度站长平台中提交死链,由于死链文件制作稍较复杂,我们可以选择规则提交的方式提交死链(处理死链过程较长,我提交的死链目前还在处理中)。
很重要的是,我们需要在自己的所有博文中修改链接,我使用了VScode的搜索关键字符功能对所有markdown文件进行了修改,效率相对较高。此外,如果使用了leancloud等第三方服务,那么也需要修改对应的url与新的相匹配,否则会造成原来数据的丢弃,还是挺可惜的。


压缩文件

关于压缩的方法,网上有很多,可以选择简易的应用。我选择的是用hexo-neat,安装插件后在站点配置文件添加如下设置,效果不错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# hexo-neat
# 博文压缩
neat_enable: true
# 压缩html
neat_html:
enable: true
exclude:
# 压缩css
neat_css:
enable: true
exclude:
- '**/*.min.css'
# 压缩js
neat_js:
enable: true
mangle: true
output:
compress:
exclude:
- '**/*.min.js'
- '**/jquery.fancybox.pack.js'
- '**/index.js'

添加完成之后,每次generate你就会在git bash终端看到neat压缩的反馈信息。
另外也有和很多网友使用的是gulp压缩,设置也很简便且有效。
压缩网站文件不仅可以提高访问加载的速度,同时减少了大量空白符,对SEO也是有不小的帮助的,推荐尝试。


主动推送

首先在根目录下安装插件npm install hexo-baidu-url-submit --save
在根目录站点配置文件中新增如下字段:

1
2
3
4
5
baidu_url_submit:
count: 100 # 提交最新的一个链接
host: gsy00517.github.io # 在百度站长平台中注册的域名
token: lY..........Fk # 请注意这是您的秘钥,所以请不要把博客源代码发布在公众仓库里!
path: baidu_urls.txt # 文本文档的地址,新链接会保存在此文本文档里

域名和秘钥可以在站长工具平台的连接提交中的接口调用地址中找到,即对应host与token后面的字段。
再把主题配置文件中的deploy修改如下:

1
2
3
4
5
6
7
8
9
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
- type: git
repo:
github: git@github.com:Gsy00517/Gsy00517.github.io.git
coding: git@git.dev.tencent.com:gsy00517/gsy00517.git
branch: master
- type: baidu_url_submitter

注意:必须严格按照上述格式,否则无法deploy。

这样以后每次执行hexo d,新的链接就会主动推送给百度,然后百度就会更快地派爬虫来发现你站点中的新链接,可以在第一时间收录新建的链接。


自动推送

自动推送是百度搜索资源平台为提高站点新增网页发现速度推出的工具,安装自动推送JS代码的网页,在页面被访问时,页面url将立即被推送给百度。详情可以查看百度的站长工具平台使用帮助。事实上,如果已经实行了主动推送,那么自动推送其实不是那么必要,因为主动推送是在生成url的时候第一时间进行推送,之后访问页面时进行的自动推送就显得晚了一步。不同推送方式的效果大概是:主动推送>自动推送>sitemap。
下面还是写一下自动推送的实现方法。
blog\themes\next\source\js\src目录下,创建名为bai.js的文件,并根据百度提供的自动推送功能方法添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
(function(){
var bp = document.createElement('script');
var curProtocol = window.location.protocol.split(':')[0];
if (curProtocol === 'https') {
bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
}
else {
bp.src = 'http://push.zhanzhang.baidu.com/push.js';
}
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();
</script>

此外,还可以blog\scaffolds目录下的模板文件post.md的分隔线之后添加这么一行:

1
<script type="text/javascript" src="/js/src/bai.js"></script>

这样以后每次创建新的文章就会自动在文末添加这行代码,即在生成的模板中包含这行代码。
如此,只要访问你的这个页面,它就会自动向百度推送你的这个网页。


sitemap

首先需要安装sitemap站点地图自动生成插件。
windows下打开git bash,输入安装命令:

1
2
npm install hexo-generator-sitemap --save
npm install hexo-generator-baidu-sitemap --save

然后在站点配置文件_config.yml中找到如下对应的位置(一般默认有,没有的话可以添加),修改如下:

1
2
3
4
5
# 自动生成sitemap
sitemap:
path: sitemap.xml
baidusitemap:
path: baidusitemap.xml

特别要注意的是,上面的path一定要缩进,否则在hexo generate时会无法编译导致报错。(似乎有一些版本的hexo不存在这样的问题,关于版本可以使用hexo version命令查看)
这样以后每次generate后都会在public目录下面生成sitemap.xml和baidusitemap.xml两个文件,即你的站点地图。也可以deploy后直接在域名后面加上这两个文件名查看你的站点地图。
在百度站长平台中,有sitemap提交的选项,由于我当初提交的网站协议前缀是http,因此xml文件所在的https前缀的链接不属于我提交的网站,而我的github page和coding page都设置了强制https访问。这个问题以后有机会再做解决,不存在这个问题的可以试试提交sitemap。


疑惑

在优化的过程中,我发现我的post模板也被改变了(原因目前未知),从原本的:

1
2
3
4
5
title: {{ title }}
date: {{ date }}
tags:
copyright: true
top:

变成了:

1
2
3
4
5
6
7
8
9
10
11
---
noteId: "9bfafbb............dd5a3"
tags: []
title:
[object Object]: null
date:
[object Object]: null
copyright: true
top: null

---

更奇怪的是,我无法删除noteId并恢复到原来的样式,每次更改保存之后又会自动给我换回来,为了方便使用,我将其修改为:

1
2
3
4
5
6
7
8
9
10
11
---
noteId: "55c6d..............d6fcf"
title:
{{ title }}
date:
{{ date }}
copyright: true
top:
categories:
tags:
---

这样就可以直接提取文章标题和创建时间了。
对于noteId的作用,网上也找不到相关信息,可能是类似于网站的ID标识的一个代号吧,我对它之后的改进以及用法可见后文。


使用noteId改进url

今天看了几个url中含有noteId的网站,立马想到其实noteId其实可以用来替代url的中文等符号从而消除乱码,这更方便了爬虫的抓取。于是,我把站点配置文件下的url设置修改如下:

同时我把模板文件post.md修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
---
noteId: 'prefix+time remember to change!!!'
title:
{{ title }}
date:
{{ date }}
copyright: true
top:
categories:
tags:
---

<script type="text/javascript" src="/js/src/bai.js"></script>

这就把我的博文网址修改成了“关键词+创建时间”的形式,当然要手动更改。
同样的,以上的操作也带来了巨大的麻烦。我需要给之前没有生成noteId的博文一一加上noteId,同时也免不了对外部辅助平台和网站内链的大幅度修改。
对于检查网站死链,我推荐一个挺实用的轻量工具Xenu,下载安装之后,选择file,然后check URL,输入网站地址,即可检查站内所有的连接中是否存在死链。下面是我仅修改了url设置而未更改内链时检测的情况,其中红色的就是死链。


修改url之后

大约是在我修改了url格式的两天后,当我再用“site:url”查询收录情况时,我发现被收录的死链已经减少了一个(似乎不是提交死链的原因,因为规则提交的死链还在处理中),然而我之前被收录、修改后原url依然可用的主页和分类页面却也消失了,这就使我非常得纳闷。这几日也查找了许多资料寻找原因,总结如下。

首先,网站url的变动产生大量死链,很有可能会导致网站排名消失,原来积累的权重大大减少甚至清除。还好目前我只是一个新站,倘若已运行并被收录了一段时间,应该要慎重考虑是否是因为网址必须得精简等原因从而放弃网站的排名。要注意的是,如果网站url链接过深,会影响搜索引擎蜘蛛抓取,时间久了,蜘蛛来的次数就会减少,最后导致网站不收录。一般建议扁平化结构,url在三层以内方便蜘蛛爬行,这在上文也提到过。
此外,如果是新站的话,收录之后消失也是正常的。事实上,上线6个月之内的网站都可以被称为新站。因为搜索引擎蜘蛛对新站有一个好奇心,发现新鲜的事物喜欢去抓取一下,这就是收录,但是收录之后会有一个审核期,包括这个收录之后又消失的问题,审核期过后如果在数据库找不到相同的信息就会认为这是一篇原创,这个时候再去看收录就又会恢复了。值得注意的是,新站上线短期内,只新增更新内容就行了,不要去改动以前的内容(特别是标题、url等,搜索引擎对这些内容很敏感)以免延长新站考核时间,当网站索引趋于稳定状态后可以适当改动。
总而言之,目前没什么好担心的,担心也没有用,还是认认真真好好地写笔记好啦!


添加robots文件

Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站可以通过Robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。
如果一个网站使用大量的js、flash、ifrmae等内容,或者如果一个网站结构混乱,那么整个网站就会是乱七八糟、毫无章法,不仅用户体验极差,更重要的是蜘蛛也不会喜欢,也没有心思去抓取网站的内容了。
robots.txt是搜索引擎蜘蛛访问网站时要查看的第一个文件,并且会根据robots.txt文件的内容来爬行网站。在某种意义上说,它的一个任务就是指导蜘蛛爬行,减少搜索引擎蜘蛛的工作量。
当搜索引擎蜘蛛访问网站时,它会首先检查该站点根目录下是否存在robots.txt文件,如果该文件存在,搜索引擎蜘蛛就会按照该文件中的内容来确定爬行的范围;如果该文件不存在,则所有的搜索引擎蜘蛛将能够访问网站上所有没有被口令保护的页面。
通常搜索引擎对网站派出的蜘蛛是有配额的,多大规模的网站放出多少蜘蛛。如果我们不配置robots文件,那么蜘蛛来到网站以后会无目的地爬行,造成的一个结果就是,需要它爬行的目录,没有爬行到,不需要爬行的,也就是我们不想被收录的内容却被爬行并放出快照。所以robots文件对于SEO具有重要的意义。
如果网站中没有robots.txt文件,则网站中的程序脚本、样式表等一些和网站内容无关的文件或目录即使被搜索引擎蜘蛛爬行,也不会增加网站的收录率和权重,只会浪费服务器资源。此外,搜索引擎派出的蜘蛛资源也是有限的,我们要做的应该是尽量让蜘蛛爬行网站重点文件、目录,最大限度的节约蜘蛛资源。
在站点根目录的source文件下添加robots.txt文件,加入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
User-agent: * 
Allow: /
Allow: /archives/
Disallow: /categories/
Disallow: /tags/
Disallow: /vendors/
Disallow: /js/
Disallow: /css/
Disallow: /fonts/
Disallow: /vendors/
Disallow: /fancybox/

Sitemap: https://gsy00517.github.io/sitemap.xml
Sitemap: https://gsy00517.github.io/baidusitemap.xml

注意sitemap中要修改成自己的url。
另外,可以在站长工具平台检测robots文件。

]]>
+

toefl笔记:首考托福——记一次裸考经历文章末尾我曾提到在被百度收录之后要好好做SEO,这段时间我也的确有所尝试与改进,因此在本文中将一些我认为比较有效的或者依旧存疑的SEO优化方法写下来,供日后参考深究。

References

电子文献:
https://blog.csdn.net/lzy98/article/details/81140704
https://www.jianshu.com/p/86557c34b671
https://baijiahao.baidu.com/s?id=1616368344109675728&wfr=spider&for=pc
https://www.jianshu.com/p/7e1166eb412a
https://baijiahao.baidu.com/s?id=1597172076743185609&wfr=spider&for=pc


SEO

之前在知乎上碰巧看到一篇别人是如何推广自己的博客的文章,里面就提到了SEO这个概念。我当时也很好奇,百度之后才发现它完全不同于CEO、CTO等概念。SEO(Search Engine Optimization),汉译为搜索引擎优化。它是一种方式,即利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。通俗的讲就是post的内容更容易被搜索引擎搜索到或者收录,且在搜索结果列表中显示靠前。
看了一圈,SEO的办法真的是多种多样,下面我就简单记录一部分我试过的方法。


优化url

同样在站点配置文件下面,可以找到站点的url设置。
如果你尚未更改过,你会发现默认的url是http://yoursite.com,我在这里吃了不少亏,之前苹果上add to favorites、RSS订阅后点开的链接以及copyright的链接都会直接跳转到yoursite而非我的博文链接。
SEO搜索引擎优化认为,网站的最佳结构是用户从首页点击三次就可以到达任何一个页面,但是我们使用hexo编译的站点默认打开文章的url是“sitename/year/mounth/day/title”四层的结构,这样的url结构很不利于SEO,爬虫就会经常爬不到我们的文章,于是,我们可以将url直接改成“sitename/title”的形式,并且title最好是用英文(中文的url会出现好多乱码,我这方面还有待改进)。
基于以上原因,我在根目录的站点配置文件下修改url设置如下(注释中是默认的):

如此,再次添加RSS订阅,就可以跟yoursite这个鬼地方say goodbye啦。
对permalink的修改将会是你的站点的一次巨大的变动,会造成大量的死链。死链会造成搜索引擎对网站的评分降低并有可能会降权。我们可以直接百度搜索“site:url”(url即你的站点网址)查看已经被搜索引擎收录的网址。如下图所示,当时我被收录了四个(截止本文最近更新前收录已达60多),其中前两个经此番调整已成为死链。


这时我们可以在百度站长平台中提交死链,由于死链文件制作稍较复杂,我们可以选择规则提交的方式提交死链(处理死链过程较长,我提交的死链目前还在处理中)。
很重要的是,我们需要在自己的所有博文中修改链接,我使用了VScode的搜索关键字符功能对所有markdown文件进行了修改,效率相对较高。此外,如果使用了leancloud等第三方服务,那么也需要修改对应的url与新的相匹配,否则会造成原来数据的丢弃,还是挺可惜的。


压缩文件

关于压缩的方法,网上有很多,可以选择简易的应用。我选择的是用hexo-neat,安装插件后在站点配置文件添加如下设置,效果不错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# hexo-neat
# 博文压缩
neat_enable: true
# 压缩html
neat_html:
enable: true
exclude:
# 压缩css
neat_css:
enable: true
exclude:
- '**/*.min.css'
# 压缩js
neat_js:
enable: true
mangle: true
output:
compress:
exclude:
- '**/*.min.js'
- '**/jquery.fancybox.pack.js'
- '**/index.js'

添加完成之后,每次generate你就会在git bash终端看到neat压缩的反馈信息。
另外也有和很多网友使用的是gulp压缩,设置也很简便且有效。
压缩网站文件不仅可以提高访问加载的速度,同时减少了大量空白符,对SEO也是有不小的帮助的,推荐尝试。


主动推送

首先在根目录下安装插件npm install hexo-baidu-url-submit --save
在根目录站点配置文件中新增如下字段:

1
2
3
4
5
baidu_url_submit:
count: 100 # 提交最新的一个链接
host: gsy00517.github.io # 在百度站长平台中注册的域名
token: lY..........Fk # 请注意这是您的秘钥,所以请不要把博客源代码发布在公众仓库里!
path: baidu_urls.txt # 文本文档的地址,新链接会保存在此文本文档里

域名和秘钥可以在站长工具平台的连接提交中的接口调用地址中找到,即对应host与token后面的字段。
再把主题配置文件中的deploy修改如下:

1
2
3
4
5
6
7
8
9
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
- type: git
repo:
github: git@github.com:Gsy00517/Gsy00517.github.io.git
coding: git@git.dev.tencent.com:gsy00517/gsy00517.git
branch: master
- type: baidu_url_submitter

注意:必须严格按照上述格式,否则无法deploy。

这样以后每次执行hexo d,新的链接就会主动推送给百度,然后百度就会更快地派爬虫来发现你站点中的新链接,可以在第一时间收录新建的链接。


自动推送

自动推送是百度搜索资源平台为提高站点新增网页发现速度推出的工具,安装自动推送JS代码的网页,在页面被访问时,页面url将立即被推送给百度。详情可以查看百度的站长工具平台使用帮助。事实上,如果已经实行了主动推送,那么自动推送其实不是那么必要,因为主动推送是在生成url的时候第一时间进行推送,之后访问页面时进行的自动推送就显得晚了一步。不同推送方式的效果大概是:主动推送>自动推送>sitemap。
下面还是写一下自动推送的实现方法。
blog\themes\next\source\js\src目录下,创建名为bai.js的文件,并根据百度提供的自动推送功能方法添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
(function(){
var bp = document.createElement('script');
var curProtocol = window.location.protocol.split(':')[0];
if (curProtocol === 'https') {
bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
}
else {
bp.src = 'http://push.zhanzhang.baidu.com/push.js';
}
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();
</script>

此外,还可以blog\scaffolds目录下的模板文件post.md的分隔线之后添加这么一行:

1
<script type="text/javascript" src="/js/src/bai.js"></script>

这样以后每次创建新的文章就会自动在文末添加这行代码,即在生成的模板中包含这行代码。
如此,只要访问你的这个页面,它就会自动向百度推送你的这个网页。


sitemap

首先需要安装sitemap站点地图自动生成插件。
windows下打开git bash,输入安装命令:

1
2
npm install hexo-generator-sitemap --save
npm install hexo-generator-baidu-sitemap --save

然后在站点配置文件_config.yml中找到如下对应的位置(一般默认有,没有的话可以添加),修改如下:

1
2
3
4
5
# 自动生成sitemap
sitemap:
path: sitemap.xml
baidusitemap:
path: baidusitemap.xml

特别要注意的是,上面的path一定要缩进,否则在hexo generate时会无法编译导致报错。(似乎有一些版本的hexo不存在这样的问题,关于版本可以使用hexo version命令查看)
这样以后每次generate后都会在public目录下面生成sitemap.xml和baidusitemap.xml两个文件,即你的站点地图。也可以deploy后直接在域名后面加上这两个文件名查看你的站点地图。
在百度站长平台中,有sitemap提交的选项,由于我当初提交的网站协议前缀是http,因此xml文件所在的https前缀的链接不属于我提交的网站,而我的github page和coding page都设置了强制https访问。这个问题以后有机会再做解决,不存在这个问题的可以试试提交sitemap。


疑惑

在优化的过程中,我发现我的post模板也被改变了(原因目前未知),从原本的:

1
2
3
4
5
title: {{ title }}
date: {{ date }}
tags:
copyright: true
top:

变成了:

1
2
3
4
5
6
7
8
9
10
11
---
noteId: "9bfafbb............dd5a3"
tags: []
title:
[object Object]: null
date:
[object Object]: null
copyright: true
top: null

---

更奇怪的是,我无法删除noteId并恢复到原来的样式,每次更改保存之后又会自动给我换回来,为了方便使用,我将其修改为:

1
2
3
4
5
6
7
8
9
10
11
---
noteId: "55c6d..............d6fcf"
title:
{{ title }}
date:
{{ date }}
copyright: true
top:
categories:
tags:
---

这样就可以直接提取文章标题和创建时间了。
对于noteId的作用,网上也找不到相关信息,可能是类似于网站的ID标识的一个代号吧,我对它之后的改进以及用法可见后文。


使用noteId改进url

今天看了几个url中含有noteId的网站,立马想到其实noteId其实可以用来替代url的中文等符号从而消除乱码,这更方便了爬虫的抓取。于是,我把站点配置文件下的url设置修改如下:

同时我把模板文件post.md修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
---
noteId: 'prefix+time remember to change!!!'
title:
{{ title }}
date:
{{ date }}
copyright: true
top:
categories:
tags:
---

<script type="text/javascript" src="/js/src/bai.js"></script>

这就把我的博文网址修改成了“关键词+创建时间”的形式,当然要手动更改。
同样的,以上的操作也带来了巨大的麻烦。我需要给之前没有生成noteId的博文一一加上noteId,同时也免不了对外部辅助平台和网站内链的大幅度修改。
对于检查网站死链,我推荐一个挺实用的轻量工具Xenu,下载安装之后,选择file,然后check URL,输入网站地址,即可检查站内所有的连接中是否存在死链。下面是我仅修改了url设置而未更改内链时检测的情况,其中红色的就是死链。


修改url之后

大约是在我修改了url格式的两天后,当我再用“site:url”查询收录情况时,我发现被收录的死链已经减少了一个(似乎不是提交死链的原因,因为规则提交的死链还在处理中),然而我之前被收录、修改后原url依然可用的主页和分类页面却也消失了,这就使我非常得纳闷。这几日也查找了许多资料寻找原因,总结如下。

首先,网站url的变动产生大量死链,很有可能会导致网站排名消失,原来积累的权重大大减少甚至清除。还好目前我只是一个新站,倘若已运行并被收录了一段时间,应该要慎重考虑是否是因为网址必须得精简等原因从而放弃网站的排名。要注意的是,如果网站url链接过深,会影响搜索引擎蜘蛛抓取,时间久了,蜘蛛来的次数就会减少,最后导致网站不收录。一般建议扁平化结构,url在三层以内方便蜘蛛爬行,这在上文也提到过。
此外,如果是新站的话,收录之后消失也是正常的。事实上,上线6个月之内的网站都可以被称为新站。因为搜索引擎蜘蛛对新站有一个好奇心,发现新鲜的事物喜欢去抓取一下,这就是收录,但是收录之后会有一个审核期,包括这个收录之后又消失的问题,审核期过后如果在数据库找不到相同的信息就会认为这是一篇原创,这个时候再去看收录就又会恢复了。值得注意的是,新站上线短期内,只新增更新内容就行了,不要去改动以前的内容(特别是标题、url等,搜索引擎对这些内容很敏感)以免延长新站考核时间,当网站索引趋于稳定状态后可以适当改动。
总而言之,目前没什么好担心的,担心也没有用,还是认认真真好好地写笔记好啦!


添加robots文件

Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站可以通过Robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。
如果一个网站使用大量的js、flash、ifrmae等内容,或者如果一个网站结构混乱,那么整个网站就会是乱七八糟、毫无章法,不仅用户体验极差,更重要的是蜘蛛也不会喜欢,也没有心思去抓取网站的内容了。
robots.txt是搜索引擎蜘蛛访问网站时要查看的第一个文件,并且会根据robots.txt文件的内容来爬行网站。在某种意义上说,它的一个任务就是指导蜘蛛爬行,减少搜索引擎蜘蛛的工作量。
当搜索引擎蜘蛛访问网站时,它会首先检查该站点根目录下是否存在robots.txt文件,如果该文件存在,搜索引擎蜘蛛就会按照该文件中的内容来确定爬行的范围;如果该文件不存在,则所有的搜索引擎蜘蛛将能够访问网站上所有没有被口令保护的页面。
通常搜索引擎对网站派出的蜘蛛是有配额的,多大规模的网站放出多少蜘蛛。如果我们不配置robots文件,那么蜘蛛来到网站以后会无目的地爬行,造成的一个结果就是,需要它爬行的目录,没有爬行到,不需要爬行的,也就是我们不想被收录的内容却被爬行并放出快照。所以robots文件对于SEO具有重要的意义。
如果网站中没有robots.txt文件,则网站中的程序脚本、样式表等一些和网站内容无关的文件或目录即使被搜索引擎蜘蛛爬行,也不会增加网站的收录率和权重,只会浪费服务器资源。此外,搜索引擎派出的蜘蛛资源也是有限的,我们要做的应该是尽量让蜘蛛爬行网站重点文件、目录,最大限度的节约蜘蛛资源。
在站点根目录的source文件下添加robots.txt文件,加入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
User-agent: * 
Allow: /
Allow: /archives/
Disallow: /categories/
Disallow: /tags/
Disallow: /vendors/
Disallow: /js/
Disallow: /css/
Disallow: /fonts/
Disallow: /vendors/
Disallow: /fancybox/

Sitemap: https://gsy00517.github.io/sitemap.xml
Sitemap: https://gsy00517.github.io/baidusitemap.xml

注意sitemap中要修改成自己的url。
另外,可以在站长工具平台检测robots文件。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>在<a href="https://gsy00517.github.io/toefl20191019150438/" target="_blan + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在<a href="https://gsy00517.github.io/toefl20191019150438/" target="_blan @@ -1689,13 +1689,13 @@ 2019-11-01T11:20:42.000Z 2020-01-19T07:36:25.647Z -

接触机器学习也有一段较长的时间了,不敢说自己全部掌握甚至精通,但是期间也了解或者尝试了许多机器学习的算法。这次就结合参考资料和我自己的感受小结一下几种机器学习的常见算法及其优点和缺点。


决策树算法

学过数据结构中的树应该对这个算法不会感到困惑,下面就简单介绍一下其优缺点。

  1. 优点

    • 易于理解和解释,可以可视化分析,容易提取出规则。
    • 可以同时处理标称型和数值型数据。
    • 测试数据集时,运行速度比较快。
    • 决策树可以很好的扩展到大型数据库中,同时它的大小独立于数据库大小。
  2. 缺点

    • 对缺失数据处理比较困难。
    • 容易出现过拟合问题,容易受到例外的干扰,对测试集非常不友好。
    • 忽略数据集中属性的相互关联。
    • ID3算法计算信息增益时结果偏向数值比较多的特征。
  3. 改进措施

    • 对决策树进行剪枝。可以采用交叉验证法和加入正则化的方法。较为理想的决策树是叶子节点数少且深度较小。
    • 使用基于决策树的combination算法,如bagging算法,randomforest算法,可以解决过拟合的问题。
  4. 常见算法

    1. C4.5算法

      ID3算法是以信息论为基础,以信息熵和信息增益度为衡量标准,从而实现对数据的归纳分类。ID3算法计算每个属性的信息增益,并选取具有最高增益的属性作为给定的测试属性。C4.5算法核心思想是ID3算法,是ID3算法的改进,改进方面有:
      • 用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足。
      • 在树构造过程中进行剪枝。
      • 能处理非离散的数据。
      • 能处理不完整的数据。

        优点

      • 产生的分类规则易于理解,准确率较高。

        缺点

      • 在构造树的过程中,需要对数据集进行多次的顺序扫描和排序,因而导致算法的低效。
      • C4.5只适合于能够驻留于内存的数据集,当训练集大得无法在内存容纳时程序无法运行。
    2. CART分类与回归树

      这是一种决策树分类方法,采用基于最小距离的基尼指数估计函数,用来决定由该子数据集生成的决策树的拓展形。如果目标变量是标称的,称为分类树;如果目标变量是连续的,称为回归树。分类树是使用树结构算法将数据分成离散类的方法。

      优点

      • 非常灵活,可以允许有部分错分成本,还可指定先验概率分布,可使用自动的成本复杂性剪枝来得到归纳性更强的树。
      • 在面对诸如存在缺失值、变量数多等问题时CART显得非常稳健。

      下面对决策树的各种算法做一个小结:
      算法支持模型树结构特征选择
      ID3分类多叉树信息增益
      C4.5分类多叉树信息增益比
      CART分类、回归二叉树基尼系数、均方差

      补充:
      信息熵:表示随机变量的不确定性,熵越大,不确定性越大。这与物理中的熵性质类似。
      信息增益:即不确定性减小的幅度。信息增益=信息熵(前)-信息熵(后)。在构造决策树的时候往往选择信息增益大的特征优先作为节点分类标准。
      信息增益比:由于仅根据信息增益构建决策树,那么三叉树以及多叉树比二叉树的效果一般来说分类效果要好,然而这很有可能会导致过拟合的问题。因此定义信息增益比=惩罚参数*信息增益。当特征个数较多时,惩罚参数较小;当特征个数较少时,惩罚参数较大,从而使信息增益比较大,进而克服信息增益偏向于选取取值较多的特征的问题。总的来说,信息增益比相对于信息增益更客观。
      基尼系数:表示集合的不确定性,基尼系数越大,则表示不平等程度越高。


分类算法

  1. KNN算法

    1. 优点

      • KNN是一种在线技术,新数据可以直接加入数据集而不必进行重新训练。
      • KNN理论简单,容易实现。实际上,KNN没有训练过程,或者说,它的训练过程就是导入数据集。
    2. 缺点

      • KNN对于样本容量大的数据集计算量比较大,极易引发维度灾难。
      • 样本不平衡时,预测偏差比较大。如:某一类的样本比较少,而其它类样本比较多。
      • KNN每一次分类都会重新进行一次全局运算,耗时久。这在实践中会非常有体会,可以参考kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集
      • 在CV领域,KNN已经被完全弃用。这是因为它不适合用来表示图像之间的视觉感知差异,如下图所示,这是CS231n中提到的一个例子,后三张图片经过不同的变换,结果与第一张原图的L2距离居然是一样的,而显然对我们而言这三张图是有很大区别的,在实际应用中往往应该区分开。
    3. 应用领域

      • 文本分类。
      • 模式识别。
      • 聚类分析。
      • 多分类领域。
  2. 支持向量机(SVM)

    支持向量机是一种基于分类边界的方法。其基本原理是(以二维数据为例):如果训练数据分布在二维平面上的点,它们按照其分类聚集在不同的区域。基于分类边界的分类算法的目标是:通过训练,找到这些分类之间的边界(直线的称为线性划分,曲线的称为非线性划分)。对于多维数据(如N维),可以将它们视为N维空间中的点,而分类边界就是N维空间中的面,称为超面(超面比N维空间少一维)。线性分类器使用超平面类型的边界,非线性分类器使用超曲面。
    支持向量机的原理是将低维空间的点映射到高维空间,使它们成为线性可分,再使用线性划分的原理来判断分类边界。在高维空间中是一种线性划分,而在原有的数据空间中,是一种非线性划分。
    在我的博文machine-learning笔记:一个支持向量机的问题中,我提及了SVM的简介与一个问题,感兴趣的话可以了解一下。
    1. 优点

      • 解决小样本下机器学习问题,相对于其他训练分类算法不需要过多样本。
      • 解决非线性问题。擅长应付线性不可分,主要用松弛变量(惩罚变量)和核函数来实现。
      • 无局部极小值问题。(相对于神经网络等算法)
      • 引入了核函数,可以很好的处理高维数据集。
      • 泛化能力比较强。结构风险最小,指分类器对问题真实模型的逼近与真实解之间的累计误差。
    2. 缺点

      • 对于核函数的高维映射解释力不强,尤其是径向基函数。
      • 对缺失数据敏感。
    3. 应用领域:

      • 文本分类。
      • 图像识别。
      • 主要二分类领域。
  3. 朴素贝叶斯算法

    朴素贝叶斯,即naive bayes。说白了就是要“sometimes naive”。
    1. 优点

      • 对大数量训练和查询时具有较高的速度。即使使用超大规模的训练集,针对每个项目通常也只会有相对较少的特征数,并且对项目的训练和分类也仅仅是特征概率的数学运算而已。
      • 支持增量式运算。即可以实时的对新增的样本进行训练。
      • 朴素贝叶斯对结果解释容易理解。
    2. 缺点

      • 由于使用了样本属性独立性的假设,所以如果样本属性有关联时其效果不好。
    3. 应用领域

      • 文本分类。
      • 欺诈检测。
  4. Logistic回归算法

    1. 优点

      • 计算代价不高,易于理解和实现。
    2. 缺点

      • 容易产生欠拟合。
      • 分类精度不高。
    3. 应用领域

      • 用于二分类领域,可以得出概率值,适用于根据分类概率排名的领域,如搜索排名等。
      • Logistic回归的扩展softmax可以应用于多分类领域,如手写字识别等。

聚类算法

  1. K-means算法

    K-means算法,即K均值算法,是一个简单的聚类算法,把n个对象根据它们的属性分为k个分割,k小于n。算法的核心就是要优化失真函数J,使其收敛到局部最小值但不是全局最小值。它比较适合凸数据集,即任意两个数据点之间的连线都在数据集内部。
    1. 算法流程

      1. 随机选择k个随机的点(称为聚类中心)。
      2. 对数据集中的每个数据点,按照距离k个中心的距离,将其与最近的中心点关联起来,与同一中心点关联的点聚成一类。
      3. 计算每一组的均值,将该组所关联的中心点移到平均值的位置。
      4. 重复第2、3两步,直到中心点不再变化。
    2. 优点

      • 算法速度很快。
    3. 缺点

      • 分组的数目k是一个输入超参数,不合适的k可能返回较差的结果。
  2. EM最大期望算法

    EM算法是基于模型的聚类方法,是在概率模型中寻找参数最大似然估计的算法,其中概率模型依赖于无法观测的隐藏变量。E步估计隐含变量,M步估计其他参数,交替将极值推向最大。
    EM算法比K-means算法计算复杂,收敛也较慢,不适于大规模数据集和高维数据,但比K-means算法计算结果稳定、准确。EM经常用在机器学习和计算机视觉的数据集聚(Data Clustering)领域。

集成算法(AdaBoost)

俗话说的好“三个臭皮匠,顶个诸葛亮”,集成算法就是将多个弱分类器集成在一起,构建一个强分类器。事实上,它可能不属于算法,而更像一种优化手段。

  1. 优点

    • 很好的利用了弱分类器进行级联。
    • 可以将不同的分类算法作为弱分类器。
    • AdaBoost具有很高的精度。
    • 相对于bagging算法和randomforest算法,AdaBoost充分考虑的每个分类器的权重。
  2. 缺点

    • AdaBoost迭代次数也就是弱分类器数目不太好设定,可以使用交叉验证来进行确定。
    • 数据不平衡导致分类精度下降。
    • 训练比较耗时,每次重新选择当前分类器最好切分点。
  3. 应用领域

    • 模式识别。
    • 计算机视觉领域。
    • 二分类和多分类场景。

神经网络算法

  1. 优点

    • 分类准确度高,学习能力极强。
    • 对噪声数据鲁棒性和容错性较强。
    • 有联想能力,能逼近任意非线性关系。
  2. 缺点

  3. 应用领域

    • 计算机视觉。
    • 自然语言处理。
    • 语音识别等。
]]>
+

接触机器学习也有一段较长的时间了,不敢说自己全部掌握甚至精通,但是期间也了解或者尝试了许多机器学习的算法。这次就结合参考资料和我自己的感受小结一下几种机器学习的常见算法及其优点和缺点。


决策树算法

学过数据结构中的树应该对这个算法不会感到困惑,下面就简单介绍一下其优缺点。

  1. 优点

    • 易于理解和解释,可以可视化分析,容易提取出规则。
    • 可以同时处理标称型和数值型数据。
    • 测试数据集时,运行速度比较快。
    • 决策树可以很好的扩展到大型数据库中,同时它的大小独立于数据库大小。
  2. 缺点

    • 对缺失数据处理比较困难。
    • 容易出现过拟合问题,容易受到例外的干扰,对测试集非常不友好。
    • 忽略数据集中属性的相互关联。
    • ID3算法计算信息增益时结果偏向数值比较多的特征。
  3. 改进措施

    • 对决策树进行剪枝。可以采用交叉验证法和加入正则化的方法。较为理想的决策树是叶子节点数少且深度较小。
    • 使用基于决策树的combination算法,如bagging算法,randomforest算法,可以解决过拟合的问题。
  4. 常见算法

    1. C4.5算法

      ID3算法是以信息论为基础,以信息熵和信息增益度为衡量标准,从而实现对数据的归纳分类。ID3算法计算每个属性的信息增益,并选取具有最高增益的属性作为给定的测试属性。C4.5算法核心思想是ID3算法,是ID3算法的改进,改进方面有:
      • 用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足。
      • 在树构造过程中进行剪枝。
      • 能处理非离散的数据。
      • 能处理不完整的数据。

        优点

      • 产生的分类规则易于理解,准确率较高。

        缺点

      • 在构造树的过程中,需要对数据集进行多次的顺序扫描和排序,因而导致算法的低效。
      • C4.5只适合于能够驻留于内存的数据集,当训练集大得无法在内存容纳时程序无法运行。
    2. CART分类与回归树

      这是一种决策树分类方法,采用基于最小距离的基尼指数估计函数,用来决定由该子数据集生成的决策树的拓展形。如果目标变量是标称的,称为分类树;如果目标变量是连续的,称为回归树。分类树是使用树结构算法将数据分成离散类的方法。

      优点

      • 非常灵活,可以允许有部分错分成本,还可指定先验概率分布,可使用自动的成本复杂性剪枝来得到归纳性更强的树。
      • 在面对诸如存在缺失值、变量数多等问题时CART显得非常稳健。

      下面对决策树的各种算法做一个小结:
      算法支持模型树结构特征选择
      ID3分类多叉树信息增益
      C4.5分类多叉树信息增益比
      CART分类、回归二叉树基尼系数、均方差

      补充:
      信息熵:表示随机变量的不确定性,熵越大,不确定性越大。这与物理中的熵性质类似。
      信息增益:即不确定性减小的幅度。信息增益=信息熵(前)-信息熵(后)。在构造决策树的时候往往选择信息增益大的特征优先作为节点分类标准。
      信息增益比:由于仅根据信息增益构建决策树,那么三叉树以及多叉树比二叉树的效果一般来说分类效果要好,然而这很有可能会导致过拟合的问题。因此定义信息增益比=惩罚参数*信息增益。当特征个数较多时,惩罚参数较小;当特征个数较少时,惩罚参数较大,从而使信息增益比较大,进而克服信息增益偏向于选取取值较多的特征的问题。总的来说,信息增益比相对于信息增益更客观。
      基尼系数:表示集合的不确定性,基尼系数越大,则表示不平等程度越高。


分类算法

  1. KNN算法

    1. 优点

      • KNN是一种在线技术,新数据可以直接加入数据集而不必进行重新训练。
      • KNN理论简单,容易实现。实际上,KNN没有训练过程,或者说,它的训练过程就是导入数据集。
    2. 缺点

      • KNN对于样本容量大的数据集计算量比较大,极易引发维度灾难。
      • 样本不平衡时,预测偏差比较大。如:某一类的样本比较少,而其它类样本比较多。
      • KNN每一次分类都会重新进行一次全局运算,耗时久。这在实践中会非常有体会,可以参考kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集
      • 在CV领域,KNN已经被完全弃用。这是因为它不适合用来表示图像之间的视觉感知差异,如下图所示,这是CS231n中提到的一个例子,后三张图片经过不同的变换,结果与第一张原图的L2距离居然是一样的,而显然对我们而言这三张图是有很大区别的,在实际应用中往往应该区分开。
    3. 应用领域

      • 文本分类。
      • 模式识别。
      • 聚类分析。
      • 多分类领域。
  2. 支持向量机(SVM)

    支持向量机是一种基于分类边界的方法。其基本原理是(以二维数据为例):如果训练数据分布在二维平面上的点,它们按照其分类聚集在不同的区域。基于分类边界的分类算法的目标是:通过训练,找到这些分类之间的边界(直线的称为线性划分,曲线的称为非线性划分)。对于多维数据(如N维),可以将它们视为N维空间中的点,而分类边界就是N维空间中的面,称为超面(超面比N维空间少一维)。线性分类器使用超平面类型的边界,非线性分类器使用超曲面。
    支持向量机的原理是将低维空间的点映射到高维空间,使它们成为线性可分,再使用线性划分的原理来判断分类边界。在高维空间中是一种线性划分,而在原有的数据空间中,是一种非线性划分。
    在我的博文machine-learning笔记:一个支持向量机的问题中,我提及了SVM的简介与一个问题,感兴趣的话可以了解一下。
    1. 优点

      • 解决小样本下机器学习问题,相对于其他训练分类算法不需要过多样本。
      • 解决非线性问题。擅长应付线性不可分,主要用松弛变量(惩罚变量)和核函数来实现。
      • 无局部极小值问题。(相对于神经网络等算法)
      • 引入了核函数,可以很好的处理高维数据集。
      • 泛化能力比较强。结构风险最小,指分类器对问题真实模型的逼近与真实解之间的累计误差。
    2. 缺点

      • 对于核函数的高维映射解释力不强,尤其是径向基函数。
      • 对缺失数据敏感。
    3. 应用领域:

      • 文本分类。
      • 图像识别。
      • 主要二分类领域。
  3. 朴素贝叶斯算法

    朴素贝叶斯,即naive bayes。说白了就是要“sometimes naive”。
    1. 优点

      • 对大数量训练和查询时具有较高的速度。即使使用超大规模的训练集,针对每个项目通常也只会有相对较少的特征数,并且对项目的训练和分类也仅仅是特征概率的数学运算而已。
      • 支持增量式运算。即可以实时的对新增的样本进行训练。
      • 朴素贝叶斯对结果解释容易理解。
    2. 缺点

      • 由于使用了样本属性独立性的假设,所以如果样本属性有关联时其效果不好。
    3. 应用领域

      • 文本分类。
      • 欺诈检测。
  4. Logistic回归算法

    1. 优点

      • 计算代价不高,易于理解和实现。
    2. 缺点

      • 容易产生欠拟合。
      • 分类精度不高。
    3. 应用领域

      • 用于二分类领域,可以得出概率值,适用于根据分类概率排名的领域,如搜索排名等。
      • Logistic回归的扩展softmax可以应用于多分类领域,如手写字识别等。

聚类算法

  1. K-means算法

    K-means算法,即K均值算法,是一个简单的聚类算法,把n个对象根据它们的属性分为k个分割,k小于n。算法的核心就是要优化失真函数J,使其收敛到局部最小值但不是全局最小值。它比较适合凸数据集,即任意两个数据点之间的连线都在数据集内部。
    1. 算法流程

      1. 随机选择k个随机的点(称为聚类中心)。
      2. 对数据集中的每个数据点,按照距离k个中心的距离,将其与最近的中心点关联起来,与同一中心点关联的点聚成一类。
      3. 计算每一组的均值,将该组所关联的中心点移到平均值的位置。
      4. 重复第2、3两步,直到中心点不再变化。
    2. 优点

      • 算法速度很快。
    3. 缺点

      • 分组的数目k是一个输入超参数,不合适的k可能返回较差的结果。
  2. EM最大期望算法

    EM算法是基于模型的聚类方法,是在概率模型中寻找参数最大似然估计的算法,其中概率模型依赖于无法观测的隐藏变量。E步估计隐含变量,M步估计其他参数,交替将极值推向最大。
    EM算法比K-means算法计算复杂,收敛也较慢,不适于大规模数据集和高维数据,但比K-means算法计算结果稳定、准确。EM经常用在机器学习和计算机视觉的数据集聚(Data Clustering)领域。

集成算法(AdaBoost)

俗话说的好“三个臭皮匠,顶个诸葛亮”,集成算法就是将多个弱分类器集成在一起,构建一个强分类器。事实上,它可能不属于算法,而更像一种优化手段。

  1. 优点

    • 很好的利用了弱分类器进行级联。
    • 可以将不同的分类算法作为弱分类器。
    • AdaBoost具有很高的精度。
    • 相对于bagging算法和randomforest算法,AdaBoost充分考虑的每个分类器的权重。
  2. 缺点

    • AdaBoost迭代次数也就是弱分类器数目不太好设定,可以使用交叉验证来进行确定。
    • 数据不平衡导致分类精度下降。
    • 训练比较耗时,每次重新选择当前分类器最好切分点。
  3. 应用领域

    • 模式识别。
    • 计算机视觉领域。
    • 二分类和多分类场景。

神经网络算法

  1. 优点

    • 分类准确度高,学习能力极强。
    • 对噪声数据鲁棒性和容错性较强。
    • 有联想能力,能逼近任意非线性关系。
  2. 缺点

  3. 应用领域

    • 计算机视觉。
    • 自然语言处理。
    • 语音识别等。
]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>接触机器学习也有一段较长的时间了,不敢说自己全部掌握甚至精通,但是期间也了解或者尝试了许多机器学习的算法。这次就结合参考资料和我自己的感受小结一 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>接触机器学习也有一段较长的时间了,不敢说自己全部掌握甚至精通,但是期间也了解或者尝试了许多机器学习的算法。这次就结合参考资料和我自己的感受小结一 @@ -1715,13 +1715,13 @@ 2019-10-19T07:04:38.000Z 2020-02-05T01:59:42.595Z -

今天上午终于把自己开学以来耿耿于怀的托福考完了。目前来看是铁定要二战了,因此下午抽空把这次考试的经历总结一下,方便再战的时候可以吸取经验和教训。


前期

大一下学期的时候去听了一个学姐的出国分享会,当然是新东方支持的,讲座已结束就被新东方的老师搞了一波传销。当时也不知道托福的有效期是两年,头脑一热就报了班和十月份的托福。不过后来想想可能暑研和暑校也用得上,或许不亏。
于是我抽了个周末去新东方校区做了一个入班小测,很幸运的是我的分能进入强化班,毕竟高中的底子还可以。然而很坑的是,我从同学那得知去年同期的同一个班报名费比我少了将近一千(我报的强化班4400rmb),简直坐地起价啊!可能时间比较早校方不是很担心没人报。
然后到了暑假,我开始零零散散地准备。在家做了一套听力一套阅读,心态就崩了,怎么这么难!感觉就是跟四六级不在一个档次上。

于是三分钟热度就被浇没了,之后就只是背单词了,打算等八月上了课听了老师的解题方法再强化练习。
到了八月中旬,开始上课了,听了几节课明白题型之后,感觉托福或许也没有想象得那么可怕,熟悉就好。那段时间回去后断断续续刷了几套TPO的阅读和听力部分。
然后开学一周,很快就到了国庆。根据我的原计划,身为拖延症重度患者的我打算在国庆力挽狂澜,为此我早早地准备好了新东方的《7天搞定托福高频核心词》,当时觉得时间充裕,计划合理,未来充满希望。然而…

等到国庆结束,我也明白了一个道理:不能被事物的表面现象所迷惑。不过,乐观的我依然觉得剩下的两周足以完成复习。
However,国庆上来劈头盖脸砸过来的课程和任务让我分身乏术。周三晚上的实验课,我做到十一点才回寝室,这更是对我的致命一击。因为回寝太晚,没时间更换被子,导致挨冻一晚上(武汉的天气太怪了)。最后,又是喉咙痛,又是犯鼻炎,那时就感觉托福基本要凉。
考前第二天,我又刷了一套新托福的阅读和听力,成绩不是特别理想。由于新东方的TPO加载速度感人,也可能是学校网络的问题,总之直到考试之前,我只在小站刷过三套TPO,在新东方刷过两套老TPO和一套新TPO。

甚至直到写这篇文章的标题之前,我都不知道托福的英文拼写是“TOEFL”(之前一直觉得是“TOFEL”)。
考试前一天,病情加重,我也就不刷题了,把上课的笔记和题型又好好熟悉了一下就早点休息了。


考前

考前问了新东方替我报名的老师,她说要打印确认信。不知道为什么我无法下载确认信成pdf格式的,最后屏幕截图打算打印,早上去考场的时候却忘记了。不过还好最后发现根本不需要确认信。
早上提早一个小时出寝室,结果发现找不到租八戒了,不知道为什么周六大家起这么早。最后租了辆摩拜拖着病体艰难地骑到了考场。
到了考试的楼下,有一个小姐姐热心地给我指路,不过我马上就发现她别有目的。她让我填一个貌似是培训机构的表格,善良易上当的我稀里糊涂地填了,本来想写个假的电话,结果感冒头很晕也没多想就如实写了,反正我平时也不怎么接陌生电话。
坐电梯到了考试的楼层,碰到我们学院一个经常见到的学长在做志愿者。他总是活跃在各种场合,好像是英语协会的,总之看到他也是开心了一小下。之后就是看序号,签到,领钥匙,去存背包。
在储物室的时候,一边的考试人员一直重复说“A考场的人可以把水拿出来”,我没听太懂。由于之前问过她我鼻炎犯了可不可以带餐巾纸(她说考场会提供),就不想再问第二次了。之前看别人的考试经历,说中途休息时可以出来喝水吃零食,还可以看写作模板,我以为是可以回到储物室的,后来才发现不能。
放完东西,我本来想再看会英语进入一下状态,结果过安检之后就只能一直在里边等了。
我们来到一个签承诺书的房间,大家都一排排坐在一种比较矮的长凳上,我拿了一张承诺书和一支笔就往后坐了。其实还可以拿个写字的时候用来垫的板子,我没注意,不过好多人和我一样都是在腿上或者趴凳子上写的。写承诺书的时候我没仔细看黑板上的要求,写错了一次,只好挺无奈地找工作人员换了一张。
不一会人就好多了,我发现这次考托福的女生比较多,大约是男生的两倍,这在我们学校很不常见啦。考试的也有大人,在我观察是不是还有培训机构的老师的时候,我的隔壁也是实验班的一个朋友也来考试了。他在C考场,那个考场更大。我的A考场人最少,相对来说环境要理想一些。不过不同考场的同学还是在同一个房间等待的。我继续观察,发现还是有几个大二面孔的,和几个人说了几句,发现还是有不少首考的人的。


入场

过了一会有一个男老师进来说一些有关考试的注意事项,说完没多久大家就到隔壁的考试教室刷脸入场了。
考场的教室和等候的教室一样,也是黄色的日光灯,看着也挺舒适。入场顺序是按照姓氏的首字母顺序的,我进去的比较早。尽管A考场大概也就二三十个人,但整个入场过程还是挺久的。
考官把我领到座位上,虽然是随机抽的但好像我的考位还是我的序号。为我把身份证插在旁边的卡槽里之后,考官又为我输了激活码进入考试界面,然后没说什么就走了。
考试的隔间挺好,靠桌子往里坐一点就完全看不到旁边了。首先是确认姓名的界面,然而考官走了我也没处问,担心确认了就直接开始考试了因此久久没敢点。由于别人还在入场,因此我不敢太早开始考试。我回头看了一眼,发现是我进候考室以来就注意到的那个男生。虽然没问过他,但看上去他这次绝对不是一战了。
过了一会,我听到有人点鼠标的声音,于是我也鼓足勇气开始点。前面大概有七八的页面都是只需continue的direction界面,而且这个界面是不会自动跳转的,我在这里停留了很久。
终于,大概第十个人入场的时候,我听到有人开始试音了。意外的是,第一个开始试音的人居然真的在介绍他生活的城市。哈哈哈看来也是首考的,不知道待会整个考场一齐开始诠释人类的本质的时候,他有什么感想。这里我暗暗庆幸自己报了班。
当大家都在诠释人类的本质时,我心里觉得还是挺可乐的。不过就在这时,我听到前面提到的那位久经沙场的老哥也开始复读了,于是我又点了一个continue。
每个continue我的拖好久才点,不过入场真的是挺久的。大概有十个人完成试音之后,我才看到了describe the city you live in,心想这个时间还是可以的。


阅读

直到考试,我才知道review的用法,点击之后,会出现一个表格,可以看到哪些题已经answered了。
阅读第一篇是关于研究者根据化石推断远古的自然环境,第二篇是什么记不太清了,好像也是历史相关的,第三篇是北美西海岸土著人的一些文化,还有配图。没有遇到加试。
总的来说,考场里效率还是比寝室要好一些的。我大概还有40分钟时做完了第一篇,到最后一篇时时间还有20多分钟,相对来说还是比较宽裕的。


听力

接下来就是听力了,我的听力是加试,有3个section。第一个section的对话我考虑太久,导致最后答lecture三道题要在一分钟之内答完。当时也只能以这个section只有50%的概率计入成绩来安慰自己。
第二个section做得还行,一些笔记还是没记到要点上,还是得多练。遗憾的是,我听力有好几篇都没听懂听力开头“you will hear part of a lecture in a ……gy class”中学科具体是什么,如果能听懂的话肯定是有一定帮助的,词汇量还不够啊!

到了第三个section,我之前在考试教室门外抽的餐巾纸用完了,我只能忍着做题,结果第三个section的lecture的conversation中的男老师似乎有异国口音,说得很不清楚,在lecture部分我也走了一会神。同样的,我发现我不是很善于掌握1个conversation+1个lecture情况下的时间,lecture的时间又分配得不多。当时也只能又以这个section只有50%的概率计入成绩来安慰自己,好吧其实两个section都凉了。
考试前两天对自己的listening还是最自信的,现在看来还是得花真功夫才行。


休息

终于休息了,我出门的时候拿到张纸条,上面提醒我11:04返场。我本来想喝口热水缓解一下我喉咙的疼痛,却被告知不能回储物室了。我这时候看到别的同学放在楼梯口的一个大桌子上的水和零食,心里真的拔凉拔凉的。我5块钱买的士力架啊!我的口语写作模板啊!不过好像大多数人都没怎么吃东西,要是我不生病的话应该也没什么问题。
考场外面的钟不是很准,我一直担心里面开始口语了我还没进去,后来发现开始第二部分的考试也是需要考官输入验证码的,因此完全不必担心。
什么都没带,我那十分钟也就上了个厕所并且补充了餐巾纸。


口语

之前开始的晚,因此我休息的时间也差不多在大家的中间。口语部分一开始的continue就比较少了,又试了一次音。这回就没有人真正介绍自己的城市了,大家又当了一回复读机。可能由于感冒造成鼻音太重的问题,我试音的音量偏低,得说得用点劲才行。
我在这里也停顿得有点久,因为待会等大家都开始说了,我就可以偷偷混投入其中以掩盖我拙劣的口语哈哈。事实上,和大家一起说真的能说得更开更自信,当大部分人说完之后,我们考场里有一个女生还在说,我明显地听到她顿了一下,然后声音顿时小了很多。
之前超牛的老哥老早就进去了(他很早就完成了听力),我进去之后本想偷听他在说什么,因为口语题都一样,结果…天呐竟然跟不上他的语速!还好这时候有一个水平不高但的确可以帮到我的吞吞吐吐的小哥开始讲了,我听到他在说work什么的,自己在脑子里构思了一下便也开始答题了。
然而,我的提前构思反倒先入为主了。当题目放出来时,我花了好久才读清题目,因为跟我想的太不一样了。以后还是不能太期望于听到别人的答案。最后,第一部分比较凉。
其实整个口语都比较凉,因为我感冒鼻音简直太重了,就像蒙着几个口罩一样,特别是其中有个录音我还咳嗽了几声。


写作

最后两部分考试感觉时间飞快,既然口语凉了,也听说写作给分还可以,我就放飞自我开始写了。
在我听听力的时候,就听到劈里啪啦的打字声了。一直对自己打字速度很有自信的我还是小惊讶了一下。
两篇作文都不是很难,我第二篇大概只写了三百词出头一些,细节还是写的有点少,都是论述的,这点下回要改进。两篇作文都是到点自动保存提交的,没有整体检查拼写,打字速度还是得练,盲打还是得加强。考场的机械键盘相对来说比较扁平,跟笔记本手感还是比较相近的。


考完

最后还有一个report成绩还是cancel的选项,考官明确说过这个不能提问,于是我看的很仔细。还好我的水平还是无压力看懂了,砸了两千块当然要report啦!出考场后还是有点不放心特地查了一下,发现还是有网友选cancel的,不过好像可以付额外的费用解决。
出考场才知道已经十二点半了,由于中途没补充零食,肚子也是饿得咕咕叫(写大作文的时候开始明显感到饿)。从储物柜拿手机的时候,不小心带了出来,掉在地上了,心里一惊,还好只在钢化膜上留下一条线,当时也觉得无所谓了。
考完也挺平静的,感觉托福考试也就这样,只可惜这次时运不济,命途多舛。以后考托福一定要在学期初考,并且好好准备,关键是要注意身体的健康!
早上起来的时候百度了一下自己的博客,发现已经被李彦宏爸爸的百度收录了,可以直接百度到我的博客和文章,也是今天比较开心的一件事吧,以后会好好做SEO的。


结果

出成绩了,更新一下。
10月19号上午考完的试,31号凌晨3:51终于刷新的成绩。查询页面显示的是“2019年10月23日的MyBest Scores”,不知道是不是23号就批好了成绩。考完后一直关注贴吧和公众号,似乎我那周是最后一次最多两周出成绩的考试,以后托福的成绩好像都会考后10天就出结果。雅思更狠,马上跟着改成了6天出成绩,它们是不是也在竞争呢…
查分的时候还是很忐忑的,没想到这次首考的成绩能到90+,虽然完全不够,但还是比我想象得要好一些的。口语果然离20还是差了一点,或许有生病的影响,但的确能体现我的水平,还是得加强练习!别人口中的提分项——写作,我也没有取得高分,看来还是不能大意,平时需要熟能生巧。但愿二战能够取得一定的进步吧!
今天去听了我们学校的海外交流项目介绍的讲座,大体上的语言成绩要求是CET4>550,CET6>500,TOEFL>80,IELTS>6.0,否则要电话面试,但这些基本都是相对来说比较中规中矩的科研项目或者学分项目,还是得努力提高英语水平啊!

]]>
+

今天上午终于把自己开学以来耿耿于怀的托福考完了。目前来看是铁定要二战了,因此下午抽空把这次考试的经历总结一下,方便再战的时候可以吸取经验和教训。


前期

大一下学期的时候去听了一个学姐的出国分享会,当然是新东方支持的,讲座已结束就被新东方的老师搞了一波传销。当时也不知道托福的有效期是两年,头脑一热就报了班和十月份的托福。不过后来想想可能暑研和暑校也用得上,或许不亏。
于是我抽了个周末去新东方校区做了一个入班小测,很幸运的是我的分能进入强化班,毕竟高中的底子还可以。然而很坑的是,我从同学那得知去年同期的同一个班报名费比我少了将近一千(我报的强化班4400rmb),简直坐地起价啊!可能时间比较早校方不是很担心没人报。
然后到了暑假,我开始零零散散地准备。在家做了一套听力一套阅读,心态就崩了,怎么这么难!感觉就是跟四六级不在一个档次上。

于是三分钟热度就被浇没了,之后就只是背单词了,打算等八月上了课听了老师的解题方法再强化练习。
到了八月中旬,开始上课了,听了几节课明白题型之后,感觉托福或许也没有想象得那么可怕,熟悉就好。那段时间回去后断断续续刷了几套TPO的阅读和听力部分。
然后开学一周,很快就到了国庆。根据我的原计划,身为拖延症重度患者的我打算在国庆力挽狂澜,为此我早早地准备好了新东方的《7天搞定托福高频核心词》,当时觉得时间充裕,计划合理,未来充满希望。然而…

等到国庆结束,我也明白了一个道理:不能被事物的表面现象所迷惑。不过,乐观的我依然觉得剩下的两周足以完成复习。
However,国庆上来劈头盖脸砸过来的课程和任务让我分身乏术。周三晚上的实验课,我做到十一点才回寝室,这更是对我的致命一击。因为回寝太晚,没时间更换被子,导致挨冻一晚上(武汉的天气太怪了)。最后,又是喉咙痛,又是犯鼻炎,那时就感觉托福基本要凉。
考前第二天,我又刷了一套新托福的阅读和听力,成绩不是特别理想。由于新东方的TPO加载速度感人,也可能是学校网络的问题,总之直到考试之前,我只在小站刷过三套TPO,在新东方刷过两套老TPO和一套新TPO。

甚至直到写这篇文章的标题之前,我都不知道托福的英文拼写是“TOEFL”(之前一直觉得是“TOFEL”)。
考试前一天,病情加重,我也就不刷题了,把上课的笔记和题型又好好熟悉了一下就早点休息了。


考前

考前问了新东方替我报名的老师,她说要打印确认信。不知道为什么我无法下载确认信成pdf格式的,最后屏幕截图打算打印,早上去考场的时候却忘记了。不过还好最后发现根本不需要确认信。
早上提早一个小时出寝室,结果发现找不到租八戒了,不知道为什么周六大家起这么早。最后租了辆摩拜拖着病体艰难地骑到了考场。
到了考试的楼下,有一个小姐姐热心地给我指路,不过我马上就发现她别有目的。她让我填一个貌似是培训机构的表格,善良易上当的我稀里糊涂地填了,本来想写个假的电话,结果感冒头很晕也没多想就如实写了,反正我平时也不怎么接陌生电话。
坐电梯到了考试的楼层,碰到我们学院一个经常见到的学长在做志愿者。他总是活跃在各种场合,好像是英语协会的,总之看到他也是开心了一小下。之后就是看序号,签到,领钥匙,去存背包。
在储物室的时候,一边的考试人员一直重复说“A考场的人可以把水拿出来”,我没听太懂。由于之前问过她我鼻炎犯了可不可以带餐巾纸(她说考场会提供),就不想再问第二次了。之前看别人的考试经历,说中途休息时可以出来喝水吃零食,还可以看写作模板,我以为是可以回到储物室的,后来才发现不能。
放完东西,我本来想再看会英语进入一下状态,结果过安检之后就只能一直在里边等了。
我们来到一个签承诺书的房间,大家都一排排坐在一种比较矮的长凳上,我拿了一张承诺书和一支笔就往后坐了。其实还可以拿个写字的时候用来垫的板子,我没注意,不过好多人和我一样都是在腿上或者趴凳子上写的。写承诺书的时候我没仔细看黑板上的要求,写错了一次,只好挺无奈地找工作人员换了一张。
不一会人就好多了,我发现这次考托福的女生比较多,大约是男生的两倍,这在我们学校很不常见啦。考试的也有大人,在我观察是不是还有培训机构的老师的时候,我的隔壁也是实验班的一个朋友也来考试了。他在C考场,那个考场更大。我的A考场人最少,相对来说环境要理想一些。不过不同考场的同学还是在同一个房间等待的。我继续观察,发现还是有几个大二面孔的,和几个人说了几句,发现还是有不少首考的人的。


入场

过了一会有一个男老师进来说一些有关考试的注意事项,说完没多久大家就到隔壁的考试教室刷脸入场了。
考场的教室和等候的教室一样,也是黄色的日光灯,看着也挺舒适。入场顺序是按照姓氏的首字母顺序的,我进去的比较早。尽管A考场大概也就二三十个人,但整个入场过程还是挺久的。
考官把我领到座位上,虽然是随机抽的但好像我的考位还是我的序号。为我把身份证插在旁边的卡槽里之后,考官又为我输了激活码进入考试界面,然后没说什么就走了。
考试的隔间挺好,靠桌子往里坐一点就完全看不到旁边了。首先是确认姓名的界面,然而考官走了我也没处问,担心确认了就直接开始考试了因此久久没敢点。由于别人还在入场,因此我不敢太早开始考试。我回头看了一眼,发现是我进候考室以来就注意到的那个男生。虽然没问过他,但看上去他这次绝对不是一战了。
过了一会,我听到有人点鼠标的声音,于是我也鼓足勇气开始点。前面大概有七八的页面都是只需continue的direction界面,而且这个界面是不会自动跳转的,我在这里停留了很久。
终于,大概第十个人入场的时候,我听到有人开始试音了。意外的是,第一个开始试音的人居然真的在介绍他生活的城市。哈哈哈看来也是首考的,不知道待会整个考场一齐开始诠释人类的本质的时候,他有什么感想。这里我暗暗庆幸自己报了班。
当大家都在诠释人类的本质时,我心里觉得还是挺可乐的。不过就在这时,我听到前面提到的那位久经沙场的老哥也开始复读了,于是我又点了一个continue。
每个continue我的拖好久才点,不过入场真的是挺久的。大概有十个人完成试音之后,我才看到了describe the city you live in,心想这个时间还是可以的。


阅读

直到考试,我才知道review的用法,点击之后,会出现一个表格,可以看到哪些题已经answered了。
阅读第一篇是关于研究者根据化石推断远古的自然环境,第二篇是什么记不太清了,好像也是历史相关的,第三篇是北美西海岸土著人的一些文化,还有配图。没有遇到加试。
总的来说,考场里效率还是比寝室要好一些的。我大概还有40分钟时做完了第一篇,到最后一篇时时间还有20多分钟,相对来说还是比较宽裕的。


听力

接下来就是听力了,我的听力是加试,有3个section。第一个section的对话我考虑太久,导致最后答lecture三道题要在一分钟之内答完。当时也只能以这个section只有50%的概率计入成绩来安慰自己。
第二个section做得还行,一些笔记还是没记到要点上,还是得多练。遗憾的是,我听力有好几篇都没听懂听力开头“you will hear part of a lecture in a ……gy class”中学科具体是什么,如果能听懂的话肯定是有一定帮助的,词汇量还不够啊!

到了第三个section,我之前在考试教室门外抽的餐巾纸用完了,我只能忍着做题,结果第三个section的lecture的conversation中的男老师似乎有异国口音,说得很不清楚,在lecture部分我也走了一会神。同样的,我发现我不是很善于掌握1个conversation+1个lecture情况下的时间,lecture的时间又分配得不多。当时也只能又以这个section只有50%的概率计入成绩来安慰自己,好吧其实两个section都凉了。
考试前两天对自己的listening还是最自信的,现在看来还是得花真功夫才行。


休息

终于休息了,我出门的时候拿到张纸条,上面提醒我11:04返场。我本来想喝口热水缓解一下我喉咙的疼痛,却被告知不能回储物室了。我这时候看到别的同学放在楼梯口的一个大桌子上的水和零食,心里真的拔凉拔凉的。我5块钱买的士力架啊!我的口语写作模板啊!不过好像大多数人都没怎么吃东西,要是我不生病的话应该也没什么问题。
考场外面的钟不是很准,我一直担心里面开始口语了我还没进去,后来发现开始第二部分的考试也是需要考官输入验证码的,因此完全不必担心。
什么都没带,我那十分钟也就上了个厕所并且补充了餐巾纸。


口语

之前开始的晚,因此我休息的时间也差不多在大家的中间。口语部分一开始的continue就比较少了,又试了一次音。这回就没有人真正介绍自己的城市了,大家又当了一回复读机。可能由于感冒造成鼻音太重的问题,我试音的音量偏低,得说得用点劲才行。
我在这里也停顿得有点久,因为待会等大家都开始说了,我就可以偷偷混投入其中以掩盖我拙劣的口语哈哈。事实上,和大家一起说真的能说得更开更自信,当大部分人说完之后,我们考场里有一个女生还在说,我明显地听到她顿了一下,然后声音顿时小了很多。
之前超牛的老哥老早就进去了(他很早就完成了听力),我进去之后本想偷听他在说什么,因为口语题都一样,结果…天呐竟然跟不上他的语速!还好这时候有一个水平不高但的确可以帮到我的吞吞吐吐的小哥开始讲了,我听到他在说work什么的,自己在脑子里构思了一下便也开始答题了。
然而,我的提前构思反倒先入为主了。当题目放出来时,我花了好久才读清题目,因为跟我想的太不一样了。以后还是不能太期望于听到别人的答案。最后,第一部分比较凉。
其实整个口语都比较凉,因为我感冒鼻音简直太重了,就像蒙着几个口罩一样,特别是其中有个录音我还咳嗽了几声。


写作

最后两部分考试感觉时间飞快,既然口语凉了,也听说写作给分还可以,我就放飞自我开始写了。
在我听听力的时候,就听到劈里啪啦的打字声了。一直对自己打字速度很有自信的我还是小惊讶了一下。
两篇作文都不是很难,我第二篇大概只写了三百词出头一些,细节还是写的有点少,都是论述的,这点下回要改进。两篇作文都是到点自动保存提交的,没有整体检查拼写,打字速度还是得练,盲打还是得加强。考场的机械键盘相对来说比较扁平,跟笔记本手感还是比较相近的。


考完

最后还有一个report成绩还是cancel的选项,考官明确说过这个不能提问,于是我看的很仔细。还好我的水平还是无压力看懂了,砸了两千块当然要report啦!出考场后还是有点不放心特地查了一下,发现还是有网友选cancel的,不过好像可以付额外的费用解决。
出考场才知道已经十二点半了,由于中途没补充零食,肚子也是饿得咕咕叫(写大作文的时候开始明显感到饿)。从储物柜拿手机的时候,不小心带了出来,掉在地上了,心里一惊,还好只在钢化膜上留下一条线,当时也觉得无所谓了。
考完也挺平静的,感觉托福考试也就这样,只可惜这次时运不济,命途多舛。以后考托福一定要在学期初考,并且好好准备,关键是要注意身体的健康!
早上起来的时候百度了一下自己的博客,发现已经被李彦宏爸爸的百度收录了,可以直接百度到我的博客和文章,也是今天比较开心的一件事吧,以后会好好做SEO的。


结果

出成绩了,更新一下。
10月19号上午考完的试,31号凌晨3:51终于刷新的成绩。查询页面显示的是“2019年10月23日的MyBest Scores”,不知道是不是23号就批好了成绩。考完后一直关注贴吧和公众号,似乎我那周是最后一次最多两周出成绩的考试,以后托福的成绩好像都会考后10天就出结果。雅思更狠,马上跟着改成了6天出成绩,它们是不是也在竞争呢…
查分的时候还是很忐忑的,没想到这次首考的成绩能到90+,虽然完全不够,但还是比我想象得要好一些的。口语果然离20还是差了一点,或许有生病的影响,但的确能体现我的水平,还是得加强练习!别人口中的提分项——写作,我也没有取得高分,看来还是不能大意,平时需要熟能生巧。但愿二战能够取得一定的进步吧!
今天去听了我们学校的海外交流项目介绍的讲座,大体上的语言成绩要求是CET4>550,CET6>500,TOEFL>80,IELTS>6.0,否则要电话面试,但这些基本都是相对来说比较中规中矩的科研项目或者学分项目,还是得努力提高英语水平啊!

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>今天上午终于把自己开学以来耿耿于怀的托福考完了。目前来看是铁定要二战了,因此下午抽空把这次考试的经历总结一下,方便再战的时候可以吸取经验和教训。 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>今天上午终于把自己开学以来耿耿于怀的托福考完了。目前来看是铁定要二战了,因此下午抽空把这次考试的经历总结一下,方便再战的时候可以吸取经验和教训。 @@ -1743,13 +1743,13 @@ 2019-10-07T15:25:12.000Z 2020-03-13T08:21:50.742Z -

接触科研,读paper是一件很头疼的事情。从当初用翻译软件两天一篇,到现在平均可以一日七八篇(放假时),我自己也有不小的感悟和进步。本文就来写一下吴恩达对于阅读ML、DL相关方面论文的建议和自己的一些感想,方便参考。


建议

首先要说明的是,这里的建议不是我想出来的,仅仅是对吴恩达提供的建议做搬运及整理。
如果你读到这里,应该也知道这个领域的先驱+巨佬Andrew Ng的大名。吴恩达(Andrew Ng),著名的美籍华裔计算机科学家,曾担任百度首席科学家,任教于Stanford,大家刚入门的时候想必都了解过或者看过由吴恩达老师讲授的斯坦福的经典课程CS229机器学习、CS230深度学习,此外,Andrew Ng还特地在网易云上为中国学生提供了中文字幕的课程(Andrew Ng英语说得比中文溜好多了哈哈)。另外,他还是著名教育平台Coursera的创始人,那里的课程更新鲜更优质,而且还可以锻炼英语能力,旁听即可。

呃放错了,不是上面那张,是这张。

对于如何阅读论文,Andrew Ng的建议是:
不要从头读到尾。相反,需要多次遍历论文。
具体有如下几个注意点:

  1. 阅读文章标题、摘要和图

    通过阅读文章标题、摘要、关键网络架构图,或许还有实验部分,你将能够对论文的概念有一个大致的了解。在深度学习中,有很多研究论文都是将整篇论文总结成一两个图形,而不需要费力地通读全文。尤其是在描述网络架构的时候,作者一般会采用比较通用的格式,读多了就会熟悉起来,比如下面DenseNet的结构:
  2. 读介绍、结论、图,略过其他

    介绍、结论和摘要是作者试图仔细总结自己工作的地方,以便向审稿人阐明为什么他们的论文应该被接受发表。
    此外,略过相关的工作部分(如果可能的话),这部分的目的是突出其他人所做的工作,这些工作在某种程度上与作者的工作有关。因此,阅读它可能是有用的,但如果不熟悉这个主题,有时会很难理解。
  3. 通读全文,但跳过数学部分

    这里我说一下我对于数学部分的处理:一般我会把重要的公式等略读一遍,然后参照着CSDN博客等网站上其他网友的解释与详解进行理解。
  4. 通读全文,但略过没有意义的部分

    Andrew Ng还解释说,当你阅读论文时(即使是最有影响力的论文),你可能也会发现有些部分没什么用,或者没什么意义。因此,如果你读了一篇论文,其中一些内容没有意义(这并不罕见),那么你可以先略读。除非你想要掌握它,那就花更多的时间。确实,当我在阅读ILSVRC、COCO等顶级比赛许多获奖模型的论文时,其中都有对比赛情况的详细结果介绍,我觉得这些部分一定程度上是可以扫读和跳读的。

感悟

目前我阅读过大多数论文的组成一般为Abstract、Introduction、Related Work、Method、Experiment、Conclusion。
可能阅历丰富的研究者能够从Abstract中提炼出比较关键的信息,但我个人认为,一篇文章最关键的是Introduction部分,该部分一般会包括前因后果和文章有哪几个contribution的总结,涵盖了本文解决的问题和一些主要概念和思路。
Related Work主要是对之前论文的分析,相当于是Introduction部分中前因的具体分析,相当于精简版的综述,如果对该领域比较了解的话完全可以跳读,但如果不熟悉该领域的话还是能从中学到很多重要的信息的。
Method部分是对Introduction部分中contribution的具体化,从这里开始会涉及到较多理论的内容,细品还是略读要靠自己拿捏。
Experiment是比较容易忽略的部分,不过有时候会提及一些trick,如果有ablation experiments的话可以重点关注一下。注意,在一些预印论文网站上的占坑论文有可能实验部分不是很完全,这是我们用于判断该篇论文结论是否可靠的依据。
Conclusion其实是Introduction的缩写,如果有未来研究方向的展望的话可以看一下。


分享

关于论文,我之前也做过一些分享,详情可以看看我之前的文章。
deep-learning笔记:开启深度学习热潮——AlexNet一文中,我提到了刚开始阅读英文论文的比较有效的方法。
deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现一文中,我也提供了许多经典模型论文的英文版、中文版、中英对照的链接。
最后要说明的是,本篇文章中Andrew Ng的建议有部分摘自公众号Datawhale的推送文章。我关注了不少这方面的公众号,筛选了几个比较优质的,在今后也会一一放到博客中推荐。

]]>
+

接触科研,读paper是一件很头疼的事情。从当初用翻译软件两天一篇,到现在平均可以一日七八篇(放假时),我自己也有不小的感悟和进步。本文就来写一下吴恩达对于阅读ML、DL相关方面论文的建议和自己的一些感想,方便参考。


建议

首先要说明的是,这里的建议不是我想出来的,仅仅是对吴恩达提供的建议做搬运及整理。
如果你读到这里,应该也知道这个领域的先驱+巨佬Andrew Ng的大名。吴恩达(Andrew Ng),著名的美籍华裔计算机科学家,曾担任百度首席科学家,任教于Stanford,大家刚入门的时候想必都了解过或者看过由吴恩达老师讲授的斯坦福的经典课程CS229机器学习、CS230深度学习,此外,Andrew Ng还特地在网易云上为中国学生提供了中文字幕的课程(Andrew Ng英语说得比中文溜好多了哈哈)。另外,他还是著名教育平台Coursera的创始人,那里的课程更新鲜更优质,而且还可以锻炼英语能力,旁听即可。

呃放错了,不是上面那张,是这张。

对于如何阅读论文,Andrew Ng的建议是:
不要从头读到尾。相反,需要多次遍历论文。
具体有如下几个注意点:

  1. 阅读文章标题、摘要和图

    通过阅读文章标题、摘要、关键网络架构图,或许还有实验部分,你将能够对论文的概念有一个大致的了解。在深度学习中,有很多研究论文都是将整篇论文总结成一两个图形,而不需要费力地通读全文。尤其是在描述网络架构的时候,作者一般会采用比较通用的格式,读多了就会熟悉起来,比如下面DenseNet的结构:
  2. 读介绍、结论、图,略过其他

    介绍、结论和摘要是作者试图仔细总结自己工作的地方,以便向审稿人阐明为什么他们的论文应该被接受发表。
    此外,略过相关的工作部分(如果可能的话),这部分的目的是突出其他人所做的工作,这些工作在某种程度上与作者的工作有关。因此,阅读它可能是有用的,但如果不熟悉这个主题,有时会很难理解。
  3. 通读全文,但跳过数学部分

    这里我说一下我对于数学部分的处理:一般我会把重要的公式等略读一遍,然后参照着CSDN博客等网站上其他网友的解释与详解进行理解。
  4. 通读全文,但略过没有意义的部分

    Andrew Ng还解释说,当你阅读论文时(即使是最有影响力的论文),你可能也会发现有些部分没什么用,或者没什么意义。因此,如果你读了一篇论文,其中一些内容没有意义(这并不罕见),那么你可以先略读。除非你想要掌握它,那就花更多的时间。确实,当我在阅读ILSVRC、COCO等顶级比赛许多获奖模型的论文时,其中都有对比赛情况的详细结果介绍,我觉得这些部分一定程度上是可以扫读和跳读的。

感悟

目前我阅读过大多数论文的组成一般为Abstract、Introduction、Related Work、Method、Experiment、Conclusion。
可能阅历丰富的研究者能够从Abstract中提炼出比较关键的信息,但我个人认为,一篇文章最关键的是Introduction部分,该部分一般会包括前因后果和文章有哪几个contribution的总结,涵盖了本文解决的问题和一些主要概念和思路。
Related Work主要是对之前论文的分析,相当于是Introduction部分中前因的具体分析,相当于精简版的综述,如果对该领域比较了解的话完全可以跳读,但如果不熟悉该领域的话还是能从中学到很多重要的信息的。
Method部分是对Introduction部分中contribution的具体化,从这里开始会涉及到较多理论的内容,细品还是略读要靠自己拿捏。
Experiment是比较容易忽略的部分,不过有时候会提及一些trick,如果有ablation experiments的话可以重点关注一下。注意,在一些预印论文网站上的占坑论文有可能实验部分不是很完全,这是我们用于判断该篇论文结论是否可靠的依据。
Conclusion其实是Introduction的缩写,如果有未来研究方向的展望的话可以看一下。


分享

关于论文,我之前也做过一些分享,详情可以看看我之前的文章。
deep-learning笔记:开启深度学习热潮——AlexNet一文中,我提到了刚开始阅读英文论文的比较有效的方法。
deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现一文中,我也提供了许多经典模型论文的英文版、中文版、中英对照的链接。
最后要说明的是,本篇文章中Andrew Ng的建议有部分摘自公众号Datawhale的推送文章。我关注了不少这方面的公众号,筛选了几个比较优质的,在今后也会一一放到博客中推荐。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>接触科研,读paper是一件很头疼的事情。从当初用翻译软件两天一篇,到现在平均可以一日七八篇(放假时),我自己也有不小的感悟和进步。本文就来写一 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>接触科研,读paper是一件很头疼的事情。从当初用翻译软件两天一篇,到现在平均可以一日七八篇(放假时),我自己也有不小的感悟和进步。本文就来写一 @@ -1773,13 +1773,13 @@ 2019-10-07T12:24:43.000Z 2020-03-10T09:59:56.091Z -

不知不觉,建站已有不少日子了,无论是内容还是界面,都逐渐丰富了起来。觉得有必要补充一篇类似于导言的文字,今天抽出点时间写一下,日后继续完善。


关于我

我来自浙江,就读于华科,目前是一名电子信息工程专业的大二本科生。从大学开始真正比较全面地接触信息技术,一年下来,在课余接触并尝试过的方面主要有编程语言python与R、linux操作系统、前端设计、机器学习与深度学习。我的博客也主要围绕这几个方面展开,具体也可以看看标签页,我也还在不断地探索与学习中。目前我主要学习的是计算机视觉方面的相关知识。
入门没多久,许多理解也还比较浅薄,博客内容主要是一些干货的搬运分享并结合自己积累的一些理解与经验,会有不足与疏漏,如果大家能给予指导,我将非常感激!今后我会尽量陆续加入更多深层次的内容。
对于这个博客网站,可以把它看作一个技术博客,而我更多的把它看成一个自己的空间,因此偶尔也会加入一些学习生活的元素,请别见怪!此外,我会尽我所能提升文章的质量,在发布后也会不断地查漏补缺,小幅修正与大幅补充结合,使每篇文章尽可能更规范易懂、更丰富充实。
或许在这个移动端主导的时代,搭网站写博客的价值有所降低。但当我写了一段时间之后,发现驱使我写博客的动力不减反增。最大的收获就是我每写一篇文章就会不断地想把相关的知识彻底搞懂,这就不断促使我去深究(哪怕钻牛角尖),因此总能收获好多写之前根本没想到的内容。


关于博客

我的文章在发布之后往往会做一些补充,此外写文章时往往把自己认为值得记录的都会写下来,因此有些文章的标题可能无法很好地概括文章的内容。可以试一试博客侧栏(手机模式下拉顶栏)的站内搜索,通过关键词或许能找到更多内容。
由于我的文章是按照最后一次更新时间早晚的顺序排列的,多了之后不方便查找和浏览,因此我新增了标签分类,或许可以帮助你更快地查看想看的内容。此外,也推荐使用我页面上的搜索功能利用关键词查找,非常便捷。

注意:PC体验更佳~

为了提高访问速度,我对我的博客进行了github+coding双线部署,url如下:
github page:https://gsy00517.github.io/
coding page:http://gsy00517.coding.me/
大家可以择优访问,事实上我并没有感到github的速度比coding慢。此外,由于我是同一个源文件双线部署,因此选择了把所有功能优先应用在github page上(比如文章打分、文章链接),但其实coding page也没有太大的区别。

注意:前期由于coding升级导致个人版page下线,好在可以迁移至新的coding,然而我尚未处理。不过github page可正常访问,也推荐访问我的github page。


关于订阅

首先,我想说的是,如果你觉得我写的内容或者方向对你有一点用处的话,非常欢迎收藏或订阅我的博客!如果你也在写博客的话,我们可以互相关注!
在侧栏,你可能会发现这样一个图标。点击之后,你会进入一个看不懂的atom.xml文件。

其实,看不懂是正常的,因为这个是给电脑看的。一个便捷的办法就是使用chrome的扩展程序添加feed(或者使用一些具有同样功能的手机app),然后打开网页时,就可以直接点击浏览器右上角的图标(会显示加号)进行订阅。这样以后每次更新了新的博文,你就可以收到提醒。


此外还可以像收藏其他网站一样进行收藏,这里不详述了。
欢迎大家常来踩踩,也欢迎大家留下评论。评论很简单,无需登录任何账号直接评论即可~
我目前也还在学习的过程中,欢迎大家和我交流,也欢迎各种批评与建议,我会努力改进!


Good News

好消息!好消息!本站已和百度、谷歌等世界知名搜索引擎达成战略合作关系!如果我写的文章有不足或疏漏之处、看完后有费解有困惑,都可以直接问度娘和谷哥就好啦~
哈哈哈皮这一下很开心。

]]>
+

不知不觉,建站已有不少日子了,无论是内容还是界面,都逐渐丰富了起来。觉得有必要补充一篇类似于导言的文字,今天抽出点时间写一下,日后继续完善。


关于我

我来自浙江,就读于华科,目前是一名电子信息工程专业的大二本科生。从大学开始真正比较全面地接触信息技术,一年下来,在课余接触并尝试过的方面主要有编程语言python与R、linux操作系统、前端设计、机器学习与深度学习。我的博客也主要围绕这几个方面展开,具体也可以看看标签页,我也还在不断地探索与学习中。目前我主要学习的是计算机视觉方面的相关知识。
入门没多久,许多理解也还比较浅薄,博客内容主要是一些干货的搬运分享并结合自己积累的一些理解与经验,会有不足与疏漏,如果大家能给予指导,我将非常感激!今后我会尽量陆续加入更多深层次的内容。
对于这个博客网站,可以把它看作一个技术博客,而我更多的把它看成一个自己的空间,因此偶尔也会加入一些学习生活的元素,请别见怪!此外,我会尽我所能提升文章的质量,在发布后也会不断地查漏补缺,小幅修正与大幅补充结合,使每篇文章尽可能更规范易懂、更丰富充实。
或许在这个移动端主导的时代,搭网站写博客的价值有所降低。但当我写了一段时间之后,发现驱使我写博客的动力不减反增。最大的收获就是我每写一篇文章就会不断地想把相关的知识彻底搞懂,这就不断促使我去深究(哪怕钻牛角尖),因此总能收获好多写之前根本没想到的内容。


关于博客

我的文章在发布之后往往会做一些补充,此外写文章时往往把自己认为值得记录的都会写下来,因此有些文章的标题可能无法很好地概括文章的内容。可以试一试博客侧栏(手机模式下拉顶栏)的站内搜索,通过关键词或许能找到更多内容。
由于我的文章是按照最后一次更新时间早晚的顺序排列的,多了之后不方便查找和浏览,因此我新增了标签分类,或许可以帮助你更快地查看想看的内容。此外,也推荐使用我页面上的搜索功能利用关键词查找,非常便捷。

注意:PC体验更佳~

为了提高访问速度,我对我的博客进行了github+coding双线部署,url如下:
github page:https://gsy00517.github.io/
coding page:http://gsy00517.coding.me/
大家可以择优访问,事实上我并没有感到github的速度比coding慢。此外,由于我是同一个源文件双线部署,因此选择了把所有功能优先应用在github page上(比如文章打分、文章链接),但其实coding page也没有太大的区别。

注意:前期由于coding升级导致个人版page下线,好在可以迁移至新的coding,然而我尚未处理。不过github page可正常访问,也推荐访问我的github page。


关于订阅

首先,我想说的是,如果你觉得我写的内容或者方向对你有一点用处的话,非常欢迎收藏或订阅我的博客!如果你也在写博客的话,我们可以互相关注!
在侧栏,你可能会发现这样一个图标。点击之后,你会进入一个看不懂的atom.xml文件。

其实,看不懂是正常的,因为这个是给电脑看的。一个便捷的办法就是使用chrome的扩展程序添加feed(或者使用一些具有同样功能的手机app),然后打开网页时,就可以直接点击浏览器右上角的图标(会显示加号)进行订阅。这样以后每次更新了新的博文,你就可以收到提醒。


此外还可以像收藏其他网站一样进行收藏,这里不详述了。
欢迎大家常来踩踩,也欢迎大家留下评论。评论很简单,无需登录任何账号直接评论即可~
我目前也还在学习的过程中,欢迎大家和我交流,也欢迎各种批评与建议,我会努力改进!


Good News

好消息!好消息!本站已和百度、谷歌等世界知名搜索引擎达成战略合作关系!如果我写的文章有不足或疏漏之处、看完后有费解有困惑,都可以直接问度娘和谷哥就好啦~
哈哈哈皮这一下很开心。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>不知不觉,建站已有不少日子了,无论是内容还是界面,都逐渐丰富了起来。觉得有必要补充一篇类似于导言的文字,今天抽出点时间写一下,日后继续完善。</ + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>不知不觉,建站已有不少日子了,无论是内容还是界面,都逐渐丰富了起来。觉得有必要补充一篇类似于导言的文字,今天抽出点时间写一下,日后继续完善。</ @@ -1795,13 +1795,13 @@ 2019-10-07T10:48:56.000Z 2019-11-03T05:44:34.547Z -

假期里想着不能让b站收藏夹里的学习资源一直吃灰,于是又刷了一遍b站的收藏夹。碰巧就看到了自己之前收藏的一种积分方法,那么这篇文章就来搬运一下这种方法的计算流程。


表格法

事实上,这种方法说白了还是分部积分法,但使用起来却要方便好多。我们直接看例子:
求解$ \int \left ( x^{2}+x \right )e^{x}dx $。

  1. 画一个两行的表格。把多项式部分写在第一行,然后把剩余的部分写在第二行。
    $ x^{2}+x\ $
    $ e^{x}\ $
  2. 接下来,我们对第一行求导,直到导数为零为止。对第二行积分,直到与第一行的0对齐为止。
    $ x^{2}+x\ $$ 2x+1\ $20
    $ e^{x}\ $$ e^{x}\ $$ e^{x}\ $$ e^{x}\ $
  3. 第三步就是交叉相乘,在本题即为第一行第一列与第二行第二列相乘,第一行第二列与第二行第三列相乘,第一行第三列与第二行第四列相乘。要注意的是,这里的交叉相乘还需要带符号,依次为正负正负正…以此类推。最后,将相乘结果相加,整理即可得到最终的解。

    注意:别忘了加上常数C。

下面再来看一个例子熟悉一下:
求解$ \int xsinxdx $。
画表格:
$ x\ $$ 1\ $$ 0\ $
$ sinx\ $$ -cosx\ $$ -sinx\ $

求解:

其实b站上还是有挺多这样的干货的,此生无悔入b站!


其它运算终止情况

看完上面的部分,细心的你肯定会想到以上的方法并不普适,仅仅适用于导数能求导至零及含有多项式因式的情况。因此,为了能更灵活地运用分部积分表格法,下面补充其它两种运算可以终止的情况。

  1. 第一行出现零元素

    这就是上面所说的含多项式的情况,也一并列写在这里,方便总览归纳。
  2. 某列函数的乘积(或它的常数倍)等于第一列

    按照分部积分的一般做法,当出现之后的某一项恰好是原来积分或者是原来积分的常数倍时,计算进入循环。这时就可以把两者移到等式的同一侧,计算出结果,这在表格法的分部积分中也是类似的。
    来看看例子:求解$ \int e^{3x}sin2xdx $。
    $ e^{3x}\ $$ 3e^{3x}\ $$ 9e^{3x}\ $
    $ sin2x\ $$ -\frac{cos2x}{2}\ $$ -\frac{sin2x}{4}\ $

    可见,第三列的乘积和第一列的乘积相差一个常数(这里是$ -\frac{9}{4} $),因此仿照之前的方法交叉相乘列出积分:移项化简可得:即为所求。
    看完这种情况,你一定会敏锐地发现,其实分部积分表格法本质上和一般的分部积分法一模一样,不过的确在使用上还是有一定的优势的。
  3. 某列的两个函数乘积(记为$ f(x) $)是一个容易计算的积分

    这种情况下,先把之前的项用之前的方法类似列出,再在结果后加上不定积分$ (-1)^{k-1}\int f(x)dx $。
    来看例子:求解$ \int x^{2}arctanxdx $。
    $ arctanx\ $$ \frac{1}{1+x^{2}}\ $
    $ x^{2}\ $$ \frac{1}{3}x^{3}\ $

    可得解:另外,当表中的第一行的某列出现多项之和,而再求导无法改变该函数或者该函数中某一项的属性,则终止表格,后再重新组合,另建表格求解。这种情况一般不会出现在题目中,如遇到再做补充。
]]>
+

假期里想着不能让b站收藏夹里的学习资源一直吃灰,于是又刷了一遍b站的收藏夹。碰巧就看到了自己之前收藏的一种积分方法,那么这篇文章就来搬运一下这种方法的计算流程。


表格法

事实上,这种方法说白了还是分部积分法,但使用起来却要方便好多。我们直接看例子:
求解$ \int \left ( x^{2}+x \right )e^{x}dx $。

  1. 画一个两行的表格。把多项式部分写在第一行,然后把剩余的部分写在第二行。
    $ x^{2}+x\ $
    $ e^{x}\ $
  2. 接下来,我们对第一行求导,直到导数为零为止。对第二行积分,直到与第一行的0对齐为止。
    $ x^{2}+x\ $$ 2x+1\ $20
    $ e^{x}\ $$ e^{x}\ $$ e^{x}\ $$ e^{x}\ $
  3. 第三步就是交叉相乘,在本题即为第一行第一列与第二行第二列相乘,第一行第二列与第二行第三列相乘,第一行第三列与第二行第四列相乘。要注意的是,这里的交叉相乘还需要带符号,依次为正负正负正…以此类推。最后,将相乘结果相加,整理即可得到最终的解。

    注意:别忘了加上常数C。

下面再来看一个例子熟悉一下:
求解$ \int xsinxdx $。
画表格:
$ x\ $$ 1\ $$ 0\ $
$ sinx\ $$ -cosx\ $$ -sinx\ $

求解:

其实b站上还是有挺多这样的干货的,此生无悔入b站!


其它运算终止情况

看完上面的部分,细心的你肯定会想到以上的方法并不普适,仅仅适用于导数能求导至零及含有多项式因式的情况。因此,为了能更灵活地运用分部积分表格法,下面补充其它两种运算可以终止的情况。

  1. 第一行出现零元素

    这就是上面所说的含多项式的情况,也一并列写在这里,方便总览归纳。
  2. 某列函数的乘积(或它的常数倍)等于第一列

    按照分部积分的一般做法,当出现之后的某一项恰好是原来积分或者是原来积分的常数倍时,计算进入循环。这时就可以把两者移到等式的同一侧,计算出结果,这在表格法的分部积分中也是类似的。
    来看看例子:求解$ \int e^{3x}sin2xdx $。
    $ e^{3x}\ $$ 3e^{3x}\ $$ 9e^{3x}\ $
    $ sin2x\ $$ -\frac{cos2x}{2}\ $$ -\frac{sin2x}{4}\ $

    可见,第三列的乘积和第一列的乘积相差一个常数(这里是$ -\frac{9}{4} $),因此仿照之前的方法交叉相乘列出积分:移项化简可得:即为所求。
    看完这种情况,你一定会敏锐地发现,其实分部积分表格法本质上和一般的分部积分法一模一样,不过的确在使用上还是有一定的优势的。
  3. 某列的两个函数乘积(记为$ f(x) $)是一个容易计算的积分

    这种情况下,先把之前的项用之前的方法类似列出,再在结果后加上不定积分$ (-1)^{k-1}\int f(x)dx $。
    来看例子:求解$ \int x^{2}arctanxdx $。
    $ arctanx\ $$ \frac{1}{1+x^{2}}\ $
    $ x^{2}\ $$ \frac{1}{3}x^{3}\ $

    可得解:另外,当表中的第一行的某列出现多项之和,而再求导无法改变该函数或者该函数中某一项的属性,则终止表格,后再重新组合,另建表格求解。这种情况一般不会出现在题目中,如遇到再做补充。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>假期里想着不能让b站收藏夹里的学习资源一直吃灰,于是又刷了一遍b站的收藏夹。碰巧就看到了自己之前收藏的一种积分方法,那么这篇文章就来搬运一下这种 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>假期里想着不能让b站收藏夹里的学习资源一直吃灰,于是又刷了一遍b站的收藏夹。碰巧就看到了自己之前收藏的一种积分方法,那么这篇文章就来搬运一下这种 @@ -1821,13 +1821,13 @@ 2019-10-07T09:29:52.000Z 2019-12-01T14:27:45.360Z -

这是一篇关于海贼王也关于游戏的文章,出于对海贼王狂热的喜爱,我决定还是在博客里添一篇这样的文章,感兴趣可以看看。


画展

国庆期间,我留在了武汉。幸运的是,海贼王官方在大陆的首次巡展“路飞来了”正好此时也在武汉开展。作为一名海贼铁粉,我当然是毫不犹豫地买了票。其实根据我博客网站的icon以及我目前的个人头像,应该很容易看出我对海贼王的热爱哈哈。

去的时候快接近饭点了,人还是不少,都是真爱啊~不过像我这样我单身一人的占比不大,但也有。让我惊讶的是当我看着日文的原稿时竟能直接反应出中文,果然那么多年来全套漫画没白买。期间跟一位貌似是艺术生的小哥聊得挺开的,只可惜最后没留联系方式,有缘再见吧。
整个展看下来还是挺震撼的,尤其是刚进去的时候,激动地鸡皮疙瘩都起来了。不过跟我在东京塔下面的海贼王主题乐园激动得哭出来还是有一定差距的。看完展之后我买了几张原稿的复刻版,花了不少钱,但觉得挺值,珍藏了。我是一个漫画党,除了剧场版或特别篇之外我看的都是漫画(不过是通过动画入坑的,星空卫视司法岛,一代人的记忆哈哈)。说实话,尾田构思之精巧,漫画史上无人能及,感兴趣可以看看知乎上关于尾田构思的讨论,漫画真的埋了很多神一般的线索,这是动画里办不到的,细细看很有意思。


燃烧之血

看完展,我心中对于海贼王的热血再一次得到激发,回学校就打开燃烧之血,回到海贼世界过把瘾。
海贼王燃烧之血(One Piece:Burning Blood)是16年发行的一款海贼王题材的格斗游戏,个人觉得其中的自然系元素化以及霸气设定真的太棒了!另外各种招式都还原得很全很细致,简直就是一边玩一边享受精彩的画面。文末提供了一些图,可以欣赏一下,真的很赞。
由于steam版价格原因(加上全部DLC需两三百rmb)以及原本这游戏好像是在游戏机上的(PC版是移植的),导致PC键盘操作方式的教程不是很全,因此本篇文章主要就是对该款游戏的按键操作做一个补充。


按键操作

十几个小时玩下来,基本的按键摸得比较熟了,其实键盘操作也有键盘的优势,熟练就好。
首先是很普适的移动方式:
前进 W
后退 S
左行 A
右行 D
下面是一些基本的战斗操作:
攻击 K
重击 O
跳跃 L
防御 ;(分号键)
往后换人 E
往前换人 I
突破极限状态 右ctrl
必杀技(突破极限状态下) 右ctrl
如果要使用招式,那么按下Q,在战斗界面的左侧就会出现招式列表,即三个招式的名称及按键操作,按住Q不松,再配合对应按键,就可以使出对应的招式。
招式一 (招式列表情况下) K
招式二 (招式列表情况下) O
招式三 (招式列表情况下) ;
一般情况下,长按对应键不松可以延迟招式的释放时间(比如在对手倒地时可以尝试)。此外,一些招式延迟附带蓄力效果,可以打出更强的攻击(附带破防效果)。
接下来是一些组合按键的操作:
破防 K+L
重击破防 O+;
侧步闪躲 W、S、A、D+;
范围攻击 S+K
范围重击 S+O
跳跃攻击 L+K
跳跃重击 L+O
有些角色还拥有特殊的衍生技能,需通过一定的按键组合释放,这里举两个例子,别的可以参考收藏图鉴:
艾斯 神火•不知火 L+O
白胡子 垂直跳跃攻击 L+O
接下来就是非常有特色的能力啦,按住P键就可以开启自然系能力者的元素化,可以轻松躲掉普攻并适时反击。如果有霸气的话,按住P也可开启霸气,在期间进行攻击就可以造成更大伤害,也不用惧怕自然系了。此外,一些角色开启能力时还可以实现特定的能力,作为海贼迷真的感动到哭,下面举几个例子:
女帝 快速后闪 P+L
黄猿 瞬移 P+L+方向
大熊 瞬移 P+L
白胡子 双震 P+招式一

白胡子的双震是我最喜欢的技能,真的非常有打击感和冲击力,上图截自b站up主的操作教程,我的许多操作都是从那学来的,他在b站和爱奇艺上都有很详细的连招教程,同时配音也很逗,感兴趣的话可以去观摩一下。


角色特点

这里补充一些目前我发现的角色的特点,也是只有海贼迷才懂的,可以说这游戏做得真的赞,后续我发现更多的话会继续补充。
1.众所周知,路飞无法被女帝石化。
2.山治对抗女性角色时,只能对女性示爱,因此只有挨打的份。


画面欣赏

静态无声的画面比起动态有声的还是差多了,但依旧不影响其魅力,看着就觉得很兴奋啦~













当然,游戏仅是起娱乐作用,劳逸结合是关键。如果你也热爱海贼王的话,欢迎和我交流!

]]>
+

这是一篇关于海贼王也关于游戏的文章,出于对海贼王狂热的喜爱,我决定还是在博客里添一篇这样的文章,感兴趣可以看看。


画展

国庆期间,我留在了武汉。幸运的是,海贼王官方在大陆的首次巡展“路飞来了”正好此时也在武汉开展。作为一名海贼铁粉,我当然是毫不犹豫地买了票。其实根据我博客网站的icon以及我目前的个人头像,应该很容易看出我对海贼王的热爱哈哈。

去的时候快接近饭点了,人还是不少,都是真爱啊~不过像我这样我单身一人的占比不大,但也有。让我惊讶的是当我看着日文的原稿时竟能直接反应出中文,果然那么多年来全套漫画没白买。期间跟一位貌似是艺术生的小哥聊得挺开的,只可惜最后没留联系方式,有缘再见吧。
整个展看下来还是挺震撼的,尤其是刚进去的时候,激动地鸡皮疙瘩都起来了。不过跟我在东京塔下面的海贼王主题乐园激动得哭出来还是有一定差距的。看完展之后我买了几张原稿的复刻版,花了不少钱,但觉得挺值,珍藏了。我是一个漫画党,除了剧场版或特别篇之外我看的都是漫画(不过是通过动画入坑的,星空卫视司法岛,一代人的记忆哈哈)。说实话,尾田构思之精巧,漫画史上无人能及,感兴趣可以看看知乎上关于尾田构思的讨论,漫画真的埋了很多神一般的线索,这是动画里办不到的,细细看很有意思。


燃烧之血

看完展,我心中对于海贼王的热血再一次得到激发,回学校就打开燃烧之血,回到海贼世界过把瘾。
海贼王燃烧之血(One Piece:Burning Blood)是16年发行的一款海贼王题材的格斗游戏,个人觉得其中的自然系元素化以及霸气设定真的太棒了!另外各种招式都还原得很全很细致,简直就是一边玩一边享受精彩的画面。文末提供了一些图,可以欣赏一下,真的很赞。
由于steam版价格原因(加上全部DLC需两三百rmb)以及原本这游戏好像是在游戏机上的(PC版是移植的),导致PC键盘操作方式的教程不是很全,因此本篇文章主要就是对该款游戏的按键操作做一个补充。


按键操作

十几个小时玩下来,基本的按键摸得比较熟了,其实键盘操作也有键盘的优势,熟练就好。
首先是很普适的移动方式:
前进 W
后退 S
左行 A
右行 D
下面是一些基本的战斗操作:
攻击 K
重击 O
跳跃 L
防御 ;(分号键)
往后换人 E
往前换人 I
突破极限状态 右ctrl
必杀技(突破极限状态下) 右ctrl
如果要使用招式,那么按下Q,在战斗界面的左侧就会出现招式列表,即三个招式的名称及按键操作,按住Q不松,再配合对应按键,就可以使出对应的招式。
招式一 (招式列表情况下) K
招式二 (招式列表情况下) O
招式三 (招式列表情况下) ;
一般情况下,长按对应键不松可以延迟招式的释放时间(比如在对手倒地时可以尝试)。此外,一些招式延迟附带蓄力效果,可以打出更强的攻击(附带破防效果)。
接下来是一些组合按键的操作:
破防 K+L
重击破防 O+;
侧步闪躲 W、S、A、D+;
范围攻击 S+K
范围重击 S+O
跳跃攻击 L+K
跳跃重击 L+O
有些角色还拥有特殊的衍生技能,需通过一定的按键组合释放,这里举两个例子,别的可以参考收藏图鉴:
艾斯 神火•不知火 L+O
白胡子 垂直跳跃攻击 L+O
接下来就是非常有特色的能力啦,按住P键就可以开启自然系能力者的元素化,可以轻松躲掉普攻并适时反击。如果有霸气的话,按住P也可开启霸气,在期间进行攻击就可以造成更大伤害,也不用惧怕自然系了。此外,一些角色开启能力时还可以实现特定的能力,作为海贼迷真的感动到哭,下面举几个例子:
女帝 快速后闪 P+L
黄猿 瞬移 P+L+方向
大熊 瞬移 P+L
白胡子 双震 P+招式一

白胡子的双震是我最喜欢的技能,真的非常有打击感和冲击力,上图截自b站up主的操作教程,我的许多操作都是从那学来的,他在b站和爱奇艺上都有很详细的连招教程,同时配音也很逗,感兴趣的话可以去观摩一下。


角色特点

这里补充一些目前我发现的角色的特点,也是只有海贼迷才懂的,可以说这游戏做得真的赞,后续我发现更多的话会继续补充。
1.众所周知,路飞无法被女帝石化。
2.山治对抗女性角色时,只能对女性示爱,因此只有挨打的份。


画面欣赏

静态无声的画面比起动态有声的还是差多了,但依旧不影响其魅力,看着就觉得很兴奋啦~













当然,游戏仅是起娱乐作用,劳逸结合是关键。如果你也热爱海贼王的话,欢迎和我交流!

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>这是一篇关于海贼王也关于游戏的文章,出于对海贼王狂热的喜爱,我决定还是在博客里添一篇这样的文章,感兴趣可以看看。</p><hr><h1 id=" + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>这是一篇关于海贼王也关于游戏的文章,出于对海贼王狂热的喜爱,我决定还是在博客里添一篇这样的文章,感兴趣可以看看。</p><hr><h1 id=" @@ -1849,13 +1849,13 @@ 2019-10-06T15:27:04.000Z 2020-03-10T10:00:47.528Z -

不久前,我对本博客网站做了一些优化,其中包括将网站同时部署到github和coding上。关于双线部署如何具体操作,网上有许多较为详尽的教程可以参考,如果有问题的话可以参考多篇不同的教程找出原因解决。在这篇文章中,我主要想讲讲我这期间遇到的一些小事项。


github与coding

考虑到每次打开博客的加载速度问题,我前几天尝试了把博客部署到coding上,实现了coding+github双线部署。coding现已经被腾讯云收购,可以直接用微信登录。
部署完成后,为了看一看效果,我使用了站长工具分别对coding和github上的网站速度进行了测试。测试结果如下:


可见,部署在coding上确实能提高一点速度。不过事实上,在实际使用中,并没有感到coding更快,搜索之后发现似乎是coding的服务器也不在内地而在香港的原因。由于coding总是会更改服务和功能,其稳定性远远不如github。不过,coding page的确能被百度更快地爬取,更新的文章能够很快地被收录。这里附上我的两个链接,可以看看效果,择优访问:
github page:https://gsy00517.github.io/
coding page:https://gsy00517.coding.me/

注意:由于版本更新,coding page现已无法正常访问,详见欢迎到访:写在前面


ssh与https

在网上的一个教程中,作者提到使用ssh比https更加稳定,尝试后暂时没有发现明显的区别,但是另一个直观的改变就是在push代码时,使用ssh url就不需要输入账号和密码。下面是我在hexo配置文件中的设置,也就是位于站点根目录下的_config.yml文件,其中后面注释中的https://github.com/Gsy00517/Gsy00517.github.io.git是原本的https url。

上面对应的ssh url一般可以从平台上直接复制获取,也可以参照我的格式进行设置。

这里简要说一说ssh与https的区别。
一般默认情况下使用的是https,除了需要在fetch和push时使用密码之外,使用https的配置比较方便。然而,使用ssh url却需要先配置和添加好ssh key,并且你必须是这个项目的拥有或者管理者,而https就没有这些要求。其实,配置ssh key也并没有那么繁琐,而且这是一劳永逸的,所以推荐还是使用ssh。
要注意的是,ssh key保存的默认位置或许会不同于网上的教程,不过可以自行更改。我的默认地址是在用户文件夹下的AppData\Roaming\SPB_16.6的ssh文件夹中。AppData文件夹默认是隐藏的,可以通过查看隐藏的项目打开。此外,如果需要经常清理temp文件的话,不妨取消这个文件夹的隐藏,这在释放windows空间中还是挺有效的,可以参见windows笔记:释放空间

key所在的文件是上图所示的第二个publisher文件,然而似乎无法直接用office打开,选择打开方式为记事本即可。
当然,如果实在找不到key所在的文件,也可以直接使用文件资源管理器的搜索功能查找名为.ssh的文件夹即可。

注:http与https的区别在于,http是明文传输的,而https是使用ssl加密的,更加安全。若要将连接提交百度站点验证,就需要使用https协议,这个在github和coding都有强制https访问的选项。


双线部署注意事项

  1. LeanCloud

    这里主要针对hexo博客双线部署后可能会出现的几个问题说明一下注意点。
    首先,如果之前使用的是LeanCloud来接受记录评论和统计阅读量的,那么为了共享数据,必须在LeanCloud控制台设置的安全中心中,添加新增的web安全域名,保存后即可解决问题。
  2. Widget

    如果使用的是基于Widget的评分系统,那么必须更改Widget设置中的domain。我是免费使用Widget,只能同时添加一个domain。我继续使用github page的域名,因此只能在我的github page中看到评分系统。
  3. 文内链接

    因为双线部署用的依旧还是同一份本地源码文件,因此在博文中提供的链接依旧是一致的。这里我也将继续使用github page的链接,也就是文内推荐的我本人的博文链接依旧还是指向github page的。事实上,这并无任何影响。
]]>
+

不久前,我对本博客网站做了一些优化,其中包括将网站同时部署到github和coding上。关于双线部署如何具体操作,网上有许多较为详尽的教程可以参考,如果有问题的话可以参考多篇不同的教程找出原因解决。在这篇文章中,我主要想讲讲我这期间遇到的一些小事项。


github与coding

考虑到每次打开博客的加载速度问题,我前几天尝试了把博客部署到coding上,实现了coding+github双线部署。coding现已经被腾讯云收购,可以直接用微信登录。
部署完成后,为了看一看效果,我使用了站长工具分别对coding和github上的网站速度进行了测试。测试结果如下:


可见,部署在coding上确实能提高一点速度。不过事实上,在实际使用中,并没有感到coding更快,搜索之后发现似乎是coding的服务器也不在内地而在香港的原因。由于coding总是会更改服务和功能,其稳定性远远不如github。不过,coding page的确能被百度更快地爬取,更新的文章能够很快地被收录。这里附上我的两个链接,可以看看效果,择优访问:
github page:https://gsy00517.github.io/
coding page:https://gsy00517.coding.me/

注意:由于版本更新,coding page现已无法正常访问,详见欢迎到访:写在前面


ssh与https

在网上的一个教程中,作者提到使用ssh比https更加稳定,尝试后暂时没有发现明显的区别,但是另一个直观的改变就是在push代码时,使用ssh url就不需要输入账号和密码。下面是我在hexo配置文件中的设置,也就是位于站点根目录下的_config.yml文件,其中后面注释中的https://github.com/Gsy00517/Gsy00517.github.io.git是原本的https url。

上面对应的ssh url一般可以从平台上直接复制获取,也可以参照我的格式进行设置。

这里简要说一说ssh与https的区别。
一般默认情况下使用的是https,除了需要在fetch和push时使用密码之外,使用https的配置比较方便。然而,使用ssh url却需要先配置和添加好ssh key,并且你必须是这个项目的拥有或者管理者,而https就没有这些要求。其实,配置ssh key也并没有那么繁琐,而且这是一劳永逸的,所以推荐还是使用ssh。
要注意的是,ssh key保存的默认位置或许会不同于网上的教程,不过可以自行更改。我的默认地址是在用户文件夹下的AppData\Roaming\SPB_16.6的ssh文件夹中。AppData文件夹默认是隐藏的,可以通过查看隐藏的项目打开。此外,如果需要经常清理temp文件的话,不妨取消这个文件夹的隐藏,这在释放windows空间中还是挺有效的,可以参见windows笔记:释放空间

key所在的文件是上图所示的第二个publisher文件,然而似乎无法直接用office打开,选择打开方式为记事本即可。
当然,如果实在找不到key所在的文件,也可以直接使用文件资源管理器的搜索功能查找名为.ssh的文件夹即可。

注:http与https的区别在于,http是明文传输的,而https是使用ssl加密的,更加安全。若要将连接提交百度站点验证,就需要使用https协议,这个在github和coding都有强制https访问的选项。


双线部署注意事项

  1. LeanCloud

    这里主要针对hexo博客双线部署后可能会出现的几个问题说明一下注意点。
    首先,如果之前使用的是LeanCloud来接受记录评论和统计阅读量的,那么为了共享数据,必须在LeanCloud控制台设置的安全中心中,添加新增的web安全域名,保存后即可解决问题。
  2. Widget

    如果使用的是基于Widget的评分系统,那么必须更改Widget设置中的domain。我是免费使用Widget,只能同时添加一个domain。我继续使用github page的域名,因此只能在我的github page中看到评分系统。
  3. 文内链接

    因为双线部署用的依旧还是同一份本地源码文件,因此在博文中提供的链接依旧是一致的。这里我也将继续使用github page的链接,也就是文内推荐的我本人的博文链接依旧还是指向github page的。事实上,这并无任何影响。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>不久前,我对本博客网站做了一些优化,其中包括将网站同时部署到github和coding上。关于双线部署如何具体操作,网上有许多较为详尽的教程可以 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>不久前,我对本博客网站做了一些优化,其中包括将网站同时部署到github和coding上。关于双线部署如何具体操作,网上有许多较为详尽的教程可以 @@ -1877,13 +1877,13 @@ 2019-10-01T10:42:16.000Z 2020-02-15T14:09:55.255Z -

之前我用pytorch把ResNet18实现了一下,但由于上周准备国家奖学金答辩没有时间来写我实现的过程与总结。今天是祖国70周年华诞,借着这股喜庆劲,把这篇文章补上。

References

电子文献:
https://blog.csdn.net/weixin_43624538/article/details/85049699
https://blog.csdn.net/u013289254/article/details/98785869

参考文献:
[1]Deep Residual Learning for Image Recognition


简介

ResNet残差网络是由何恺明等四名微软研究院的华人提出的,当初看到论文标题下面的中国名字还是挺高兴的。文章引入部分,作者就探讨了深度神经网络的优化是否就只是叠加层数、增加深度那么简单。显然这是不可能的,增加深度带来的首要问题就是梯度爆炸、消散的问题,这是由于随着层数的增多,在网络中反向传播的梯度会随着连乘变得不稳定,从而变得特别大或者特别小。其中以梯度消散更为常见。值得注意的是,论文中还提到深度更深的网络反而出现准确率下降并不是由过拟合所引起的。
为了解决这个问题,研究者们做出了很多思考与尝试,其中的代表有relu激活函数的使用,Batch Normalization的使用等。关于这两种方法,可以参考网上的资料以及我的博文deep-learning笔记:开启深度学习热潮——AlexNetdeep-learning笔记:学习率衰减与批归一化
对于上面这个问题,ResNet作出的贡献是引入skip/identity connection。如下所示就是两个基本的残差模块。

上面这个block可表示为:$ F(X)=H(X)-x $。在这里,X为浅层输出,H(x)为深层的输出。当浅层的X代表的特征已经足够成熟,即当任何对于特征X的改变都会让loss变大时,F(X)会自动趋向于学习成为0,X则从恒等映射的路径继续传递。
这样,我们就可以在不增加计算成本的情况下使得在前向传递过程中,如果浅层的输出已经足够成熟(optimal),那么就让深层网络后面的层仅实现恒等映射的作用。
当X与F(X)通道数目不同时,作者尝试了两种identity mapping的方式。一种即对X缺失的通道直接补零从而使其能够对齐,这种方式比较简单直接,无需额外的参数;另一种则是通过使用1x1的conv来映射从而使通道也能达成一致。


论文

老规矩,这里还是先呈上我用黄色荧光高亮出我认为比较重要的要点的论文原文,这里我只有英文版
如果需要没有被我标注过的原文,可以直接搜索,这里我仅提供一次,可以点击这里下载。
不过,虽然没有pdf中文版,但其实深度学习CV方向一些比较经典的论文的英文、中文、中英对照都可以到Deep Learning Papers Translation上看到,非常方便。


自己实现

在论文中,作者提到了如下几个ResNet的版本的结构。

这里我实现的是ResNet18。
由于这不是我第一次使用pytorch进行实现,一些基本的使用操作我就不加注释了,想看注释来理解的话可以参考我之前VGG的实现。
由于残差的引入,导致ResNet的结构比较复杂,而论文中并没有非常详细的阐述,在研究官方源码之后,我对它的结构才有了完整的了解,这里我画出来以便参考。

注:此模块在2016年何大神的论文中给出了新的改进,可以参考我的博文deep-learning笔记:记首次ResNet实战

ResNet18的每一layer包括了两个这样的basic block,其中1x1的卷积核仅在X与F(X)通道数目不一致时进行操作,在我的代码中,我定义shortcut函数来对应一切通道一致、无需处理的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import torch
import torch.nn as nn
import torch.nn.functional as F

class ResNet(nn.Module):
def __init__(self):
super(ResNet, self).__init__()

self.conv1 = nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 7, stride = 2, padding = 3, bias = False)
self.max = nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)

self.bn1 = nn.BatchNorm2d(64)
self.bn2 = nn.BatchNorm2d(64)
self.bn3 = nn.BatchNorm2d(128)
self.bn4 = nn.BatchNorm2d(256)
self.bn5 = nn.BatchNorm2d(512)

self.shortcut = nn.Sequential()
self.shortcut3 = nn.Sequential(nn.Conv2d(64, 128, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(128))
self.shortcut4 = nn.Sequential(nn.Conv2d(128, 256, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(256))
self.shortcut5 = nn.Sequential(nn.Conv2d(256, 512, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(512))

self.conv2 = nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv3_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv3_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv4_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv4_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv5_1 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv5_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.avg = nn.AdaptiveAvgPool2d((1, 1))
#adaptive自适应,只给定输入和输出大小,让机器自行调整选择核尺寸和步长大小

self.fc = nn.Linear(512, 1000)

def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x1 = self.max(x)

#layer1
x = F.relu(self.bn2(self.conv2(x1)))
x = self.bn2(self.conv2(x))
x += self.shortcut(x1) #pytorch0.4.0之后这里要改为x = x + self.shortcut(x1)
x2 = F.relu(x)

x = F.relu(self.bn2(self.conv2(x2)))
x = self.bn2(self.conv2(x))
x += self.shortcut(x2)
x3 = F.relu(x)

#layer2
x = F.relu(self.bn3(self.conv3_1(x3)))
x = self.bn3(self.conv3_2(x))
x += self.shortcut3(x3)
x4 = F.relu(x)

x = F.relu(self.bn3(self.conv3_2(x4)))
x = self.bn3(self.conv3_2(x))
x += self.shortcut(x4)
x5 = F.relu(x)

#layer3
x = F.relu(self.bn4(self.conv4_1(x5)))
x = self.bn4(self.conv4_2(x))
x += self.shortcut4(x5)
x6 = F.relu(x)

x = F.relu(self.bn4(self.conv4_2(x6)))
x = self.bn4(self.conv4_2(x))
x += self.shortcut(x6)
x7 = F.relu(x)

#layer4
x = F.relu(self.bn5(self.conv5_1(x7)))
x = self.bn5(self.conv5_2(x))
x += self.shortcut5(x7)
x8 = F.relu(x)

x = F.relu(self.bn5(self.conv5_2(x8)))
x = self.bn5(self.conv5_2(x))
x += self.shortcut(x8)
x = F.relu(x)

#ending
x = self.avg(x)

#变换维度,可以设其中一个尺寸为-1,表示机器内部自己计算,但同时只能有一个为-1
x = x.view(-1, self.num_flat_features(x))
x = self.fc(x)

x = F.softmax(x, dim = 1)

return x

def num_flat_features(self, x):
size = x.size()[1:]
num_features = 1
for s in size:
num_features *= s
return num_features

net = ResNet()

同样的,我们可以随机生成一个张量来进行验证:

1
2
3
input = torch.randn(1, 3, 48, 48)
out = net(input)
print(out)

如果可以顺利地输出,那么模型基本上是没有问题的。


出现的问题

在这里我还是想把自己踩的一些简单的坑记下来,引以为戒。

  1. softmax输出全为1

    当我使用F.softmax之后,出现了这样的一个问题:

    查找资料后发现,我错误的把对每一行softmax当作了对每一列softmax。因为这个softmax语句是我从之前的自己做的一道kaggle题目写的代码中ctrl+C+V过来的,复制过来的是x = F.softmax(x, dim = 0),在这里,dim = 0意味着我对张量的每一列进行softmax,这是因为我之前的场景中需要处理的张量是一维的,也就是tensor()里面只有一对“[]”,此时它默认只有一列,我对列进行softmax自然就没有问题。
    而放到这里,我再对列进行softmax时,每列上就只有一个元素。那么结果就都是1即100%了。解决的方法就是把dim设为1。
    下面我在用一组代码直观地展示一下softmax的用法与区别。

    1
    2
    3
    4
    5
    6
    7
    import torch
    import torch.nn.functional as F
    x1= torch.Tensor( [ [1, 2, 3, 4], [1, 3, 4, 5], [3, 4, 5, 6]])
    y11= F.softmax(x1, dim = 0) #对每一列进行softmax
    y12 = F.softmax(x1, dim = 1) #对每一行进行softmax
    x2 = torch.Tensor([1, 2, 3, 4])
    y2 = F.softmax(x2, dim = 0)

    我们输出每个结果,可以看到:

  2. bias

    或许你可以发现,在我的代码中,每个卷积层中都设置了bias = False,这是我在参考官方源码之后补上的。那么,这个bias是什么,又有什么用呢?
    我们在学深度学习的时候,最早接触到的神经网络应该是感知器,它的结构如下图所示。 要想激活这个感知器,就必须使x1 * w1 + x2 * w2 + ... + xn * wn > T(T为一个阈值),而T越大,想激活这个感知器的难度越大。
    考虑样本较多的情况,我不可能手动选择一个阈值,使得模型整体表现最佳,因此我们不如使得T变成可学习的,这样一来,T会自动学习到一个数,使得模型的整体表现最佳。当把T移动到左边,它就成了bias偏置,x1 * w1 + x2 * w2 + ... + xn * wn - T > 0。显然,偏置的大小控制着激活这个感知器的难易程度。
    在比感知器高级的神经网络中,也是如此。
    但倘若我们要在卷积后面加上归一化操作,那么bias的作用就无法体现了。
    我们以ResNet卷积层后的BN层为例。
    可参考我的上一篇博文,BN处理过程中有这样一步: 对于分子而言,无论有没有bias,对结果都没有影响;而对于下面分母而言,因为是方差操作,所以也没有影响。因此,在ResNet中,因为每次卷积之后都要进行BN操作,那就不需要启用bias,否则非但不起作用,还会消耗一定的显卡内存。

官方源码

如果你此时对ResNet的结构已经有了比较清晰的理解,那么可以尝试着来理解一下官方源码的思路。其实我觉得先看像我这样直观的代码实现再看官方源码更有助理解且更高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import torch
import torch.nn as nn
from .utils import load_state_dict_from_url


__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',
'resnet152', 'resnext50_32x4d', 'resnext101_32x8d',
'wide_resnet50_2', 'wide_resnet101_2']


model_urls = {
'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
'resnext50_32x4d': 'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth',
'resnext101_32x8d': 'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth',
'wide_resnet50_2': 'https://download.pytorch.org/models/wide_resnet50_2-95faca4d.pth',
'wide_resnet101_2': 'https://download.pytorch.org/models/wide_resnet101_2-32ee1156.pth',
}


def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)


def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)


class BasicBlock(nn.Module):
expansion = 1
__constants__ = ['downsample']

def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride

def forward(self, x):
identity = x

out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)

out = self.conv2(out)
out = self.bn2(out)

if self.downsample is not None:
identity = self.downsample(x)

out += identity
out = self.relu(out)

return out


class Bottleneck(nn.Module):
expansion = 4

def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride

def forward(self, x):
identity = x

out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)

out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)

out = self.conv3(out)
out = self.bn3(out)

if self.downsample is not None:
identity = self.downsample(x)

out += identity
out = self.relu(out)

return out


class ResNet(nn.Module):

def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer

self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2,
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)

for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)

# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)

def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)

layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))

return nn.Sequential(*layers)

def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)

x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)

x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)

return x


def _resnet(arch, block, layers, pretrained, progress, **kwargs):
model = ResNet(block, layers, **kwargs)
if pretrained:
state_dict = load_state_dict_from_url(model_urls[arch],
progress=progress)
model.load_state_dict(state_dict)
return model


def resnet18(pretrained=False, progress=True, **kwargs):
r"""ResNet-18 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress,
**kwargs)


def resnet34(pretrained=False, progress=True, **kwargs):
r"""ResNet-34 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress,
**kwargs)


def resnet50(pretrained=False, progress=True, **kwargs):
r"""ResNet-50 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress,
**kwargs)


def resnet101(pretrained=False, progress=True, **kwargs):
r"""ResNet-101 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained, progress,
**kwargs)


def resnet152(pretrained=False, progress=True, **kwargs):
r"""ResNet-152 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained, progress,
**kwargs)


def resnext50_32x4d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-50 32x4d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 4
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)


def resnext101_32x8d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-101 32x8d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 8
return _resnet('resnext101_32x8d', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)


def wide_resnet50_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-50-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_

The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet50_2', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)


def wide_resnet101_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-101-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_

The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet101_2', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)


pth文件

在阅读官方源码时,我们会注意到官方提供了一系列版本的model_urls,其中,每一个url都是以.pth结尾的。
当我下载了对应的文件之后,并不知道如何处理,于是我通过搜索,简单的了解了pth文件的概念与使用方法。
简单来说,pth文件就是一个表示Python的模块搜索路径(module search path)的文本文件,在xxx.pth文件里面,会书写一些路径,一行一个。如果我们将xxx.pth文件放在特定位置,则可以让python在加载模块时,读取xxx.pth中指定的路径。
下面我使用pytorch对pth文件进行加载操作。
首先,我把ResNet18对应的pth文件下载到桌面。

1
2
3
4
5
6
7
8
9
10
11
import torch

import torchvision.models as models

# pretrained = True就可以使用预训练的模型
net = models.resnet18(pretrained = False)
#注意,根据model的不同,这里models.xxx的内容也是不同的,比如models.squeezenet1_1

pthfile = r'C:\Users\sheny\Desktop\resnet18-5c106cde.pth'#pth文件所在路径
net.load_state_dict(torch.load(pthfile))
print(net)

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)

这样你就可以看到很详尽的参数设置了。
我们还可以加载所有的参数。

1
2
3
4
5
6
7
import torch

pthfile = r'C:\Users\sheny\Desktop\resnet18-5c106cde.pth'

net = torch.load(pthfile)

print(net)

输出如下:

1
2
3
4
5
6
7
8
OrderedDict([('conv1.weight', Parameter containing:
tensor([[[[-1.0419e-02, -6.1356e-03, -1.8098e-03, ..., 5.6615e-02,
1.7083e-02, -1.2694e-02],
[ 1.1083e-02, 9.5276e-03, -1.0993e-01, ..., -2.7124e-01,
-1.2907e-01, 3.7424e-03],
[-6.9434e-03, 5.9089e-02, 2.9548e-01, ..., 5.1972e-01,
2.5632e-01, 6.3573e-02],
...,

]]>
+

之前我用pytorch把ResNet18实现了一下,但由于上周准备国家奖学金答辩没有时间来写我实现的过程与总结。今天是祖国70周年华诞,借着这股喜庆劲,把这篇文章补上。

References

电子文献:
https://blog.csdn.net/weixin_43624538/article/details/85049699
https://blog.csdn.net/u013289254/article/details/98785869

参考文献:
[1]Deep Residual Learning for Image Recognition


简介

ResNet残差网络是由何恺明等四名微软研究院的华人提出的,当初看到论文标题下面的中国名字还是挺高兴的。文章引入部分,作者就探讨了深度神经网络的优化是否就只是叠加层数、增加深度那么简单。显然这是不可能的,增加深度带来的首要问题就是梯度爆炸、消散的问题,这是由于随着层数的增多,在网络中反向传播的梯度会随着连乘变得不稳定,从而变得特别大或者特别小。其中以梯度消散更为常见。值得注意的是,论文中还提到深度更深的网络反而出现准确率下降并不是由过拟合所引起的。
为了解决这个问题,研究者们做出了很多思考与尝试,其中的代表有relu激活函数的使用,Batch Normalization的使用等。关于这两种方法,可以参考网上的资料以及我的博文deep-learning笔记:开启深度学习热潮——AlexNetdeep-learning笔记:学习率衰减与批归一化
对于上面这个问题,ResNet作出的贡献是引入skip/identity connection。如下所示就是两个基本的残差模块。

上面这个block可表示为:$ F(X)=H(X)-x $。在这里,X为浅层输出,H(x)为深层的输出。当浅层的X代表的特征已经足够成熟,即当任何对于特征X的改变都会让loss变大时,F(X)会自动趋向于学习成为0,X则从恒等映射的路径继续传递。
这样,我们就可以在不增加计算成本的情况下使得在前向传递过程中,如果浅层的输出已经足够成熟(optimal),那么就让深层网络后面的层仅实现恒等映射的作用。
当X与F(X)通道数目不同时,作者尝试了两种identity mapping的方式。一种即对X缺失的通道直接补零从而使其能够对齐,这种方式比较简单直接,无需额外的参数;另一种则是通过使用1x1的conv来映射从而使通道也能达成一致。


论文

老规矩,这里还是先呈上我用黄色荧光高亮出我认为比较重要的要点的论文原文,这里我只有英文版
如果需要没有被我标注过的原文,可以直接搜索,这里我仅提供一次,可以点击这里下载。
不过,虽然没有pdf中文版,但其实深度学习CV方向一些比较经典的论文的英文、中文、中英对照都可以到Deep Learning Papers Translation上看到,非常方便。


自己实现

在论文中,作者提到了如下几个ResNet的版本的结构。

这里我实现的是ResNet18。
由于这不是我第一次使用pytorch进行实现,一些基本的使用操作我就不加注释了,想看注释来理解的话可以参考我之前VGG的实现。
由于残差的引入,导致ResNet的结构比较复杂,而论文中并没有非常详细的阐述,在研究官方源码之后,我对它的结构才有了完整的了解,这里我画出来以便参考。

注:此模块在2016年何大神的论文中给出了新的改进,可以参考我的博文deep-learning笔记:记首次ResNet实战

ResNet18的每一layer包括了两个这样的basic block,其中1x1的卷积核仅在X与F(X)通道数目不一致时进行操作,在我的代码中,我定义shortcut函数来对应一切通道一致、无需处理的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import torch
import torch.nn as nn
import torch.nn.functional as F

class ResNet(nn.Module):
def __init__(self):
super(ResNet, self).__init__()

self.conv1 = nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 7, stride = 2, padding = 3, bias = False)
self.max = nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)

self.bn1 = nn.BatchNorm2d(64)
self.bn2 = nn.BatchNorm2d(64)
self.bn3 = nn.BatchNorm2d(128)
self.bn4 = nn.BatchNorm2d(256)
self.bn5 = nn.BatchNorm2d(512)

self.shortcut = nn.Sequential()
self.shortcut3 = nn.Sequential(nn.Conv2d(64, 128, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(128))
self.shortcut4 = nn.Sequential(nn.Conv2d(128, 256, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(256))
self.shortcut5 = nn.Sequential(nn.Conv2d(256, 512, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(512))

self.conv2 = nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv3_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv3_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv4_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv4_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv5_1 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv5_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.avg = nn.AdaptiveAvgPool2d((1, 1))
#adaptive自适应,只给定输入和输出大小,让机器自行调整选择核尺寸和步长大小

self.fc = nn.Linear(512, 1000)

def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x1 = self.max(x)

#layer1
x = F.relu(self.bn2(self.conv2(x1)))
x = self.bn2(self.conv2(x))
x += self.shortcut(x1) #pytorch0.4.0之后这里要改为x = x + self.shortcut(x1)
x2 = F.relu(x)

x = F.relu(self.bn2(self.conv2(x2)))
x = self.bn2(self.conv2(x))
x += self.shortcut(x2)
x3 = F.relu(x)

#layer2
x = F.relu(self.bn3(self.conv3_1(x3)))
x = self.bn3(self.conv3_2(x))
x += self.shortcut3(x3)
x4 = F.relu(x)

x = F.relu(self.bn3(self.conv3_2(x4)))
x = self.bn3(self.conv3_2(x))
x += self.shortcut(x4)
x5 = F.relu(x)

#layer3
x = F.relu(self.bn4(self.conv4_1(x5)))
x = self.bn4(self.conv4_2(x))
x += self.shortcut4(x5)
x6 = F.relu(x)

x = F.relu(self.bn4(self.conv4_2(x6)))
x = self.bn4(self.conv4_2(x))
x += self.shortcut(x6)
x7 = F.relu(x)

#layer4
x = F.relu(self.bn5(self.conv5_1(x7)))
x = self.bn5(self.conv5_2(x))
x += self.shortcut5(x7)
x8 = F.relu(x)

x = F.relu(self.bn5(self.conv5_2(x8)))
x = self.bn5(self.conv5_2(x))
x += self.shortcut(x8)
x = F.relu(x)

#ending
x = self.avg(x)

#变换维度,可以设其中一个尺寸为-1,表示机器内部自己计算,但同时只能有一个为-1
x = x.view(-1, self.num_flat_features(x))
x = self.fc(x)

x = F.softmax(x, dim = 1)

return x

def num_flat_features(self, x):
size = x.size()[1:]
num_features = 1
for s in size:
num_features *= s
return num_features

net = ResNet()

同样的,我们可以随机生成一个张量来进行验证:

1
2
3
input = torch.randn(1, 3, 48, 48)
out = net(input)
print(out)

如果可以顺利地输出,那么模型基本上是没有问题的。


出现的问题

在这里我还是想把自己踩的一些简单的坑记下来,引以为戒。

  1. softmax输出全为1

    当我使用F.softmax之后,出现了这样的一个问题:

    查找资料后发现,我错误的把对每一行softmax当作了对每一列softmax。因为这个softmax语句是我从之前的自己做的一道kaggle题目写的代码中ctrl+C+V过来的,复制过来的是x = F.softmax(x, dim = 0),在这里,dim = 0意味着我对张量的每一列进行softmax,这是因为我之前的场景中需要处理的张量是一维的,也就是tensor()里面只有一对“[]”,此时它默认只有一列,我对列进行softmax自然就没有问题。
    而放到这里,我再对列进行softmax时,每列上就只有一个元素。那么结果就都是1即100%了。解决的方法就是把dim设为1。
    下面我在用一组代码直观地展示一下softmax的用法与区别。

    1
    2
    3
    4
    5
    6
    7
    import torch
    import torch.nn.functional as F
    x1= torch.Tensor( [ [1, 2, 3, 4], [1, 3, 4, 5], [3, 4, 5, 6]])
    y11= F.softmax(x1, dim = 0) #对每一列进行softmax
    y12 = F.softmax(x1, dim = 1) #对每一行进行softmax
    x2 = torch.Tensor([1, 2, 3, 4])
    y2 = F.softmax(x2, dim = 0)

    我们输出每个结果,可以看到:

  2. bias

    或许你可以发现,在我的代码中,每个卷积层中都设置了bias = False,这是我在参考官方源码之后补上的。那么,这个bias是什么,又有什么用呢?
    我们在学深度学习的时候,最早接触到的神经网络应该是感知器,它的结构如下图所示。 要想激活这个感知器,就必须使x1 * w1 + x2 * w2 + ... + xn * wn > T(T为一个阈值),而T越大,想激活这个感知器的难度越大。
    考虑样本较多的情况,我不可能手动选择一个阈值,使得模型整体表现最佳,因此我们不如使得T变成可学习的,这样一来,T会自动学习到一个数,使得模型的整体表现最佳。当把T移动到左边,它就成了bias偏置,x1 * w1 + x2 * w2 + ... + xn * wn - T > 0。显然,偏置的大小控制着激活这个感知器的难易程度。
    在比感知器高级的神经网络中,也是如此。
    但倘若我们要在卷积后面加上归一化操作,那么bias的作用就无法体现了。
    我们以ResNet卷积层后的BN层为例。
    可参考我的上一篇博文,BN处理过程中有这样一步: 对于分子而言,无论有没有bias,对结果都没有影响;而对于下面分母而言,因为是方差操作,所以也没有影响。因此,在ResNet中,因为每次卷积之后都要进行BN操作,那就不需要启用bias,否则非但不起作用,还会消耗一定的显卡内存。

官方源码

如果你此时对ResNet的结构已经有了比较清晰的理解,那么可以尝试着来理解一下官方源码的思路。其实我觉得先看像我这样直观的代码实现再看官方源码更有助理解且更高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import torch
import torch.nn as nn
from .utils import load_state_dict_from_url


__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',
'resnet152', 'resnext50_32x4d', 'resnext101_32x8d',
'wide_resnet50_2', 'wide_resnet101_2']


model_urls = {
'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
'resnext50_32x4d': 'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth',
'resnext101_32x8d': 'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth',
'wide_resnet50_2': 'https://download.pytorch.org/models/wide_resnet50_2-95faca4d.pth',
'wide_resnet101_2': 'https://download.pytorch.org/models/wide_resnet101_2-32ee1156.pth',
}


def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)


def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)


class BasicBlock(nn.Module):
expansion = 1
__constants__ = ['downsample']

def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride

def forward(self, x):
identity = x

out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)

out = self.conv2(out)
out = self.bn2(out)

if self.downsample is not None:
identity = self.downsample(x)

out += identity
out = self.relu(out)

return out


class Bottleneck(nn.Module):
expansion = 4

def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride

def forward(self, x):
identity = x

out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)

out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)

out = self.conv3(out)
out = self.bn3(out)

if self.downsample is not None:
identity = self.downsample(x)

out += identity
out = self.relu(out)

return out


class ResNet(nn.Module):

def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer

self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2,
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)

for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)

# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)

def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)

layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))

return nn.Sequential(*layers)

def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)

x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)

x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)

return x


def _resnet(arch, block, layers, pretrained, progress, **kwargs):
model = ResNet(block, layers, **kwargs)
if pretrained:
state_dict = load_state_dict_from_url(model_urls[arch],
progress=progress)
model.load_state_dict(state_dict)
return model


def resnet18(pretrained=False, progress=True, **kwargs):
r"""ResNet-18 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress,
**kwargs)


def resnet34(pretrained=False, progress=True, **kwargs):
r"""ResNet-34 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress,
**kwargs)


def resnet50(pretrained=False, progress=True, **kwargs):
r"""ResNet-50 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress,
**kwargs)


def resnet101(pretrained=False, progress=True, **kwargs):
r"""ResNet-101 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained, progress,
**kwargs)


def resnet152(pretrained=False, progress=True, **kwargs):
r"""ResNet-152 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained, progress,
**kwargs)


def resnext50_32x4d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-50 32x4d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 4
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)


def resnext101_32x8d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-101 32x8d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 8
return _resnet('resnext101_32x8d', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)


def wide_resnet50_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-50-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_

The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet50_2', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)


def wide_resnet101_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-101-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_

The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet101_2', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)


pth文件

在阅读官方源码时,我们会注意到官方提供了一系列版本的model_urls,其中,每一个url都是以.pth结尾的。
当我下载了对应的文件之后,并不知道如何处理,于是我通过搜索,简单的了解了pth文件的概念与使用方法。
简单来说,pth文件就是一个表示Python的模块搜索路径(module search path)的文本文件,在xxx.pth文件里面,会书写一些路径,一行一个。如果我们将xxx.pth文件放在特定位置,则可以让python在加载模块时,读取xxx.pth中指定的路径。
下面我使用pytorch对pth文件进行加载操作。
首先,我把ResNet18对应的pth文件下载到桌面。

1
2
3
4
5
6
7
8
9
10
11
import torch

import torchvision.models as models

# pretrained = True就可以使用预训练的模型
net = models.resnet18(pretrained = False)
#注意,根据model的不同,这里models.xxx的内容也是不同的,比如models.squeezenet1_1

pthfile = r'C:\Users\sheny\Desktop\resnet18-5c106cde.pth'#pth文件所在路径
net.load_state_dict(torch.load(pthfile))
print(net)

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)

这样你就可以看到很详尽的参数设置了。
我们还可以加载所有的参数。

1
2
3
4
5
6
7
import torch

pthfile = r'C:\Users\sheny\Desktop\resnet18-5c106cde.pth'

net = torch.load(pthfile)

print(net)

输出如下:

1
2
3
4
5
6
7
8
OrderedDict([('conv1.weight', Parameter containing:
tensor([[[[-1.0419e-02, -6.1356e-03, -1.8098e-03, ..., 5.6615e-02,
1.7083e-02, -1.2694e-02],
[ 1.1083e-02, 9.5276e-03, -1.0993e-01, ..., -2.7124e-01,
-1.2907e-01, 3.7424e-03],
[-6.9434e-03, 5.9089e-02, 2.9548e-01, ..., 5.1972e-01,
2.5632e-01, 6.3573e-02],
...,

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>之前我用pytorch把ResNet18实现了一下,但由于上周准备国家奖学金答辩没有时间来写我实现的过程与总结。今天是祖国70周年华诞,借着这股 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前我用pytorch把ResNet18实现了一下,但由于上周准备国家奖学金答辩没有时间来写我实现的过程与总结。今天是祖国70周年华诞,借着这股 @@ -1892,14 +1892,14 @@ - - - - + + + + @@ -1913,13 +1913,13 @@ 2019-10-01T07:14:54.000Z 2020-02-26T23:46:40.829Z -

一段时间之前,在一个深度学习交流群里看到一个群友发问:为什么他的训练误差最后疯狂上下抖动而不是一直降低。

作为一个很萌的萌新,我当时也很疑惑。但后来我结合所学,仔细思考之后,发现这是一个挺容易犯的错误。

References

电子文献:
https://blog.csdn.net/bestrivern/article/details/86301619
https://www.jianshu.com/p/9643cba47655
https://www.cnblogs.com/eilearn/p/9780696.html
https://blog.csdn.net/donkey_1993/article/details/81871132
https://www.pytorchtutorial.com/how-to-use-batchnorm/


问题

事实上,这是一个在机器学习中就有可能遇到的问题,当学习速率α设置得过大时,往往在模型训练的后期难以达到最优解,而是在最优解附近来回抖动。还有可能反而使损失函数越来越大,甚至达到无穷,如下图所示。

而在深度学习中,假设我们使用mini-batch梯度下降法,由于mini-batch的数量不大,大概64或者128个样本,在迭代过程中会有噪声。这个时候使用固定的学习率导致的结果就是虽然下降朝向最小值,但不会精确地收敛,只会在附近不断地波动(蓝色线)。

但如果慢慢减少学习率,在初期,学习还是相对较快地,但随着学习率的变小,步伐也会变慢变小,所以最后当开始收敛时,你的曲线(绿色线)会在最小值附近的一个较小区域之内摆动,而不是在训练过程中,大幅度地在最小值附近摆动。

对于这个问题,我目前收集了有下面这些解决办法。


直接修改学习率

在吴恩达的机器学习课程中,他介绍了一种人为选择学习率的规则:每三倍选择一个学习率。
比如:我们首先选择了0.1为学习率,那么当这个学习率过大时,我们修改成0.3。倘若还是偏大,我们继续改为0.01、0.003、0.001…以此类推,当学习率偏小是也是以三倍增加并尝试检验,最终选出比较合适的学习率。
但这种方法只适用于模型数量小的情况,且这种方法终究还是固定的学习率,依旧无法很好地权衡从而达到前期快速下降与后期稳定收敛的目的。


学习率动态衰减

学习率衰减的本质在于,在学习初期,你能承受并且需要较大的步伐,但当开始收敛的时候,小一些的学习率能让你步伐小一些,从而更稳定地达到精确的最优解。
为此,我们另外增添衰减率超参数,构建函数使学习率能够在训练的过程中动态衰减。

其中decay rate称为衰减率,epoch num是代数,$ \alpha _{0} $是初始学习率。
此外还有下面这些构造方法:
指数衰减:$ \alpha =0.95^{epochnum}*\alpha _{0} $
其他常用方法:

其中k为mini-batch的数字。


几种衰减方法的实现

在pytorch中,学习率调整主要有两种方式:
1.直接修改optimizer中的lr参数。
2.利用lr_scheduler()提供的几种衰减函数。即使用torch.optim.lr_scheduler,基于循环的次数提供了一些方法来调节学习率。
3.利用torch.optim.lr_scheduler.ReduceLROnPlateau,基于验证测量结果来设置不同的学习率.
下面提供几种实现方法:
准备(对下列通用):

1
2
3
4
5
6
7
8
9
10
11
import torch
from torch.optim import * #包含Adam,lr_scheduler等
import torch.nn as nn

#生成一个简单全连接神经网络
class net(nn.Module):
def __init__(self):
super(net, self).__init__()
self.fc = nn.Linear(1, 10)
def forward(self, x):
return self.fc(x)

  1. 手动阶梯式衰减

    1
    2
    3
    4
    5
    6
    7
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    for epoch in range(100):
    if epoch % 5 == 0:
    for p in optimizer.param_groups:
    p['lr'] *= 0.9 #学习率超参的位置:optimizer.state_dict()['param_groups'][0]['lr']

    这里是每过5个epoch就进行一次衰减。

  2. lambda自定义衰减

    1
    2
    3
    4
    5
    6
    7
    8
    import numpy as np 
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    lambda1 = lambda epoch: np.sin(epoch) / epoch
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda = lambda1)
    for epoch in range(100):
    scheduler.step()

    lr_lambda会接收到一个int参数:epoch,然后根据epoch计算出对应的lr。如果设置多个lambda函数的话,会分别作用于optimizer中的不同的params_group。

  3. StepLR阶梯式衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.StepLR(optimizer, step_size = 5, gamma = 0.8)
    for epoch in range(100):
    scheduler.step()

    每个epoch,lr会自动乘以gamma。

  4. 三段式衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.MultiStepLR(optimizer, milestones = [20,80], gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是,当epoch进入milestones范围内即乘以gamma,离开milestones范围之后再乘以gamma。
    这种衰减方式也是在学术论文中最常见的方式,一般手动调整也会采用这种方法。

  5. 连续衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ExponentialLR(optimizer, gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是在每个epoch中lr都乘以gamma,从而达到连续衰减的效果。

  6. 余弦式调整

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max = 20)
    for epoch in range(100):
    scheduler.step()

    这里的T_max对应1/2个cos周期所对应的epoch数值。

  7. 基于loss和accuracy

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode = 'min', factor = 0.1, patience = 10, verbose = False, threshold = 0.0001, threshold_mode = 'rel', cooldown = 0, min_lr = 0, eps = 1e-08)
    for epoch in range(100):
    scheduler.step()

    当发现loss不再降低或者accuracy不再提高之后,就降低学习率。

    注:上面代码中各参数意义如下:
    mode:’min’模式检测metric是否不再减小,’max’模式检测metric是否不再增大;
    factor:触发条件后lr*=factor;
    patience:不再减小(或增大)的累计次数;
    verbose:触发条件后print;
    threshold:只关注超过阈值的显著变化;
    threshold_mode:有rel和abs两种阈值计算模式,rel规则:max模式下如果超过best(1+threshold)为显著,min模式下如果低于best(1-threshold)为显著;abs规则:max模式下如果超过best+threshold为显著,min模式下如果低于best-threshold为显著;
    cooldown:触发一次条件后,等待一定epoch再进行检测,避免lr下降过速;
    min_lr:最小的允许lr;
    eps:如果新旧lr之间的差异小与1e-8,则忽略此次更新。

这里非常感谢facebook的员工给我们提供了如此多的选择与便利!
对于上述方法如有任何疑惑,还请查阅torch.optim文档


批归一化(Batch Normalization)

除了对学习率进行调整之外,Batch Normalization也可以有效地解决之前的问题。
我是在学习ResNet的时候第一次遇到批归一化这个概念的。随着深度神经网络深度的加深,训练越来越困难,收敛越来越慢。为此,很多论文都尝试解决这个问题,比如ReLU激活函数,再比如Residual Network,而BN本质上也是解释并从某个不同的角度来解决这个问题的。
通过使用Batch Normalization,我们可以加快网络的收敛速度,这样我们就可以使用较大的学习率来训练网络了。此外,BN还提高了网络的泛化能力。
BN的基本思想其实相当直观:
首先,因为深层神经网络在做非线性变换前的激活输入值(就是x=WU+B,U是输入)随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,之所以训练收敛慢,一般是整体分布逐渐往非线性函数的取值区间的上下限两端靠近(对于Sigmoid函数来说,意味着激活输入值WU+B是大的负值或正值),这就导致了反向传播时低层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因。
事实上,神经网络学习过程本质上是为了学习数据的分布,而BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0、方差为1的标准正态分布,其实就是把越来越偏的分布强制拉回比较标准的分布,这样使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,从而让梯度变大,避免梯度消失问题产生,而且梯度变大意味着学习收敛速度快,因此通过BN能大大加快训练速度。
下面来看看BN的具体操作过程:

即以下四个步骤:
1.计算样本均值。
2.计算样本方差。
3.对样本数据进行标准化处理。
4.进行平移和缩放处理。这里引入了γ和β两个参数。通过训练可学习重构的γ和β这两个参数,让我们的网络可以学习恢复出原始网络所要学习的特征分布。
下面是BN层的训练流程:

这里的详细过程如下:
输入:待进入激活函数的变量。
输出:
1.这里的K,在卷积网络中可以看作是卷积核个数,如网络中第n层有64个卷积核,就需要计算64次。

注意:在正向传播时,会使用γ与β使得BN层输出与输入一样。

2.在反向传播时利用γ与β求得梯度从而改变训练权值(变量)。
3.通过不断迭代直到训练结束,求得关于不同层的γ与β。
4.不断遍历训练集中的图片,取出每个batch_size中的γ与β,最后统计每层BN的γ与β各自的和除以图片数量得到平均值,并对其做无偏估计直作为每一层的E[x]与Var[x]。
5.在预测的正向传播时,对测试数据求取γ与β,并使用该层的E[x]与Var[x],通过图中11:所表示的公式计算BN层输出。

注意:在预测时,BN层的输出已经被改变,因此BN层在预测中的作用体现在此处。

上面输入的是待进入激活函数的变量,在残差网络ResNet中,的确也是先经过BN层再用relu函数做非线性处理的。那么,为什么BN层一般用在线性层和卷积层的后面,而不是放在非线性单元即激活函数之后呢?
因为非线性单元的输出分布形状会在训练过程中变化,归一化无法消除他的方差偏移。相反的,全连接和卷积层的输出一般是一个对称、非稀疏的一个分布,更加类似高斯分布,对他们进行归一化会产生更加稳定的分布。
比如,我们对一个高斯分布的数据relu激活,那么小于0的直接就被抑制了,这样得到的结果很难是高斯分布了,这时候再添加一个BN层就很难达到所需的效果。
很多实验证明,BatchNorm只要用了就有效果,所以在一般情况下没有理由不用。但也有相反的情况,比如当每个batch里所有的sample都非常相似的时候,相似到mean和variance都基本为0时,最好不要用BatchNorm。此外如果batch size为1,从原理上来讲,此时用BatchNorm是没有任何意义的。

注意:通常我们在进行Transfer Learning的时候,会冻结之前的网络权重,注意这时候往往也会冻结BatchNorm中训练好的moving averages值。这些moving averages值只适用于以前的旧的数据,对新数据不一定适用。所以最好的方法是在Transfer Learning的时候不要冻结BatchNorm层,让moving averages值重新从新的数据中学习。


批归一化实现

这里还是使用pytorch进行实现。
准备(对下列通用):

1
2
import torch
import torch.nn as nn

  1. 2d或3d输入

    1
    2
    3
    4
    5
    6
    # 添加了可学习的仿射变换参数
    m = nn.BatchNorm1d(100)
    # 未添加可学习的仿射变换参数
    m = nn.BatchNorm1d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100))
    output = m(input)

    我们查看m,可以看到有如下形式:

    1
    BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=False, track_running_stats=True)

    这里解释一下涉及到的参数:
    num_features:来自期望输入的特征数,该期望输入的大小为:batch_size * num_features(* width)
    eps:为保证数值稳定性(分母不能趋近或取0),给分母加上的值,默认为1e-5。
    momentum:计算动态均值和动态方差并进行移动平均所使用的动量,默认为0.1。
    affine:一个布尔值,当设为true时,就给该层添加可学习的仿射变换参数。仿射变换将在后文做简单介绍。
    BatchNorm1d可以有两种输入输出:
    1.输入(N,C),输出(N,C)。
    2.输入(N,C,L),输出(N,C,L)。

  2. 3d或4d输入

    1
    2
    3
    4
    5
    m = nn.BatchNorm2d(100)
    #或者
    m = nn.BatchNorm2d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100, 35, 45))
    output = m(input)

    BatchNorm2d也可以有两种输入输出:
    1.输入(N,C,L),输出(N,C,L)。
    2.输入(N,C,H,W),输出(N,C,H,W)。

  3. 4d或5d输入

    1
    2
    3
    m = nn.BatchNorm3d(100)
    #或者
    m = nn.BatchNorm3d(100, affine=False)

    BatchNorm3d同样支持两种输入输出:
    1.输入(N,C,H,W),输出(N,C,H,W)。
    2.输入(N,C,D,H,W),输出(N,C,D,H,W)。


仿射变换

这里我简单介绍一下仿射变换的概念,仿射变换(Affine Transformation或Affine Map)是一种二维坐标(x, y)到二维坐标(u, v)的变换,它是另外两种简单变换的叠加,一是线性变换,二是平移变换。同时,仿射变换保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点确定一个唯一的仿射变换。

补充:
共线性:若几个点变换前在一条线上,则仿射变换后仍然在一条线上。
平行性:若两条线变换前平行,则变换后仍然平行。
共线比例不变性:变换前一条线上两条线段的比例,在变换后比例不变。

在二维图像变换中,它的一般表达如下:

可以视为线性变换R和平移变换T的叠加。
另外,仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切。因此我们可以将几种简单的变换矩阵相乘来实现仿射变换。

]]>
+

一段时间之前,在一个深度学习交流群里看到一个群友发问:为什么他的训练误差最后疯狂上下抖动而不是一直降低。

作为一个很萌的萌新,我当时也很疑惑。但后来我结合所学,仔细思考之后,发现这是一个挺容易犯的错误。

References

电子文献:
https://blog.csdn.net/bestrivern/article/details/86301619
https://www.jianshu.com/p/9643cba47655
https://www.cnblogs.com/eilearn/p/9780696.html
https://blog.csdn.net/donkey_1993/article/details/81871132
https://www.pytorchtutorial.com/how-to-use-batchnorm/


问题

事实上,这是一个在机器学习中就有可能遇到的问题,当学习速率α设置得过大时,往往在模型训练的后期难以达到最优解,而是在最优解附近来回抖动。还有可能反而使损失函数越来越大,甚至达到无穷,如下图所示。

而在深度学习中,假设我们使用mini-batch梯度下降法,由于mini-batch的数量不大,大概64或者128个样本,在迭代过程中会有噪声。这个时候使用固定的学习率导致的结果就是虽然下降朝向最小值,但不会精确地收敛,只会在附近不断地波动(蓝色线)。

但如果慢慢减少学习率,在初期,学习还是相对较快地,但随着学习率的变小,步伐也会变慢变小,所以最后当开始收敛时,你的曲线(绿色线)会在最小值附近的一个较小区域之内摆动,而不是在训练过程中,大幅度地在最小值附近摆动。

对于这个问题,我目前收集了有下面这些解决办法。


直接修改学习率

在吴恩达的机器学习课程中,他介绍了一种人为选择学习率的规则:每三倍选择一个学习率。
比如:我们首先选择了0.1为学习率,那么当这个学习率过大时,我们修改成0.3。倘若还是偏大,我们继续改为0.01、0.003、0.001…以此类推,当学习率偏小是也是以三倍增加并尝试检验,最终选出比较合适的学习率。
但这种方法只适用于模型数量小的情况,且这种方法终究还是固定的学习率,依旧无法很好地权衡从而达到前期快速下降与后期稳定收敛的目的。


学习率动态衰减

学习率衰减的本质在于,在学习初期,你能承受并且需要较大的步伐,但当开始收敛的时候,小一些的学习率能让你步伐小一些,从而更稳定地达到精确的最优解。
为此,我们另外增添衰减率超参数,构建函数使学习率能够在训练的过程中动态衰减。

其中decay rate称为衰减率,epoch num是代数,$ \alpha _{0} $是初始学习率。
此外还有下面这些构造方法:
指数衰减:$ \alpha =0.95^{epochnum}*\alpha _{0} $
其他常用方法:

其中k为mini-batch的数字。


几种衰减方法的实现

在pytorch中,学习率调整主要有两种方式:
1.直接修改optimizer中的lr参数。
2.利用lr_scheduler()提供的几种衰减函数。即使用torch.optim.lr_scheduler,基于循环的次数提供了一些方法来调节学习率。
3.利用torch.optim.lr_scheduler.ReduceLROnPlateau,基于验证测量结果来设置不同的学习率.
下面提供几种实现方法:
准备(对下列通用):

1
2
3
4
5
6
7
8
9
10
11
import torch
from torch.optim import * #包含Adam,lr_scheduler等
import torch.nn as nn

#生成一个简单全连接神经网络
class net(nn.Module):
def __init__(self):
super(net, self).__init__()
self.fc = nn.Linear(1, 10)
def forward(self, x):
return self.fc(x)

  1. 手动阶梯式衰减

    1
    2
    3
    4
    5
    6
    7
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    for epoch in range(100):
    if epoch % 5 == 0:
    for p in optimizer.param_groups:
    p['lr'] *= 0.9 #学习率超参的位置:optimizer.state_dict()['param_groups'][0]['lr']

    这里是每过5个epoch就进行一次衰减。

  2. lambda自定义衰减

    1
    2
    3
    4
    5
    6
    7
    8
    import numpy as np 
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    lambda1 = lambda epoch: np.sin(epoch) / epoch
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda = lambda1)
    for epoch in range(100):
    scheduler.step()

    lr_lambda会接收到一个int参数:epoch,然后根据epoch计算出对应的lr。如果设置多个lambda函数的话,会分别作用于optimizer中的不同的params_group。

  3. StepLR阶梯式衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.StepLR(optimizer, step_size = 5, gamma = 0.8)
    for epoch in range(100):
    scheduler.step()

    每个epoch,lr会自动乘以gamma。

  4. 三段式衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.MultiStepLR(optimizer, milestones = [20,80], gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是,当epoch进入milestones范围内即乘以gamma,离开milestones范围之后再乘以gamma。
    这种衰减方式也是在学术论文中最常见的方式,一般手动调整也会采用这种方法。

  5. 连续衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ExponentialLR(optimizer, gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是在每个epoch中lr都乘以gamma,从而达到连续衰减的效果。

  6. 余弦式调整

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max = 20)
    for epoch in range(100):
    scheduler.step()

    这里的T_max对应1/2个cos周期所对应的epoch数值。

  7. 基于loss和accuracy

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode = 'min', factor = 0.1, patience = 10, verbose = False, threshold = 0.0001, threshold_mode = 'rel', cooldown = 0, min_lr = 0, eps = 1e-08)
    for epoch in range(100):
    scheduler.step()

    当发现loss不再降低或者accuracy不再提高之后,就降低学习率。

    注:上面代码中各参数意义如下:
    mode:’min’模式检测metric是否不再减小,’max’模式检测metric是否不再增大;
    factor:触发条件后lr*=factor;
    patience:不再减小(或增大)的累计次数;
    verbose:触发条件后print;
    threshold:只关注超过阈值的显著变化;
    threshold_mode:有rel和abs两种阈值计算模式,rel规则:max模式下如果超过best(1+threshold)为显著,min模式下如果低于best(1-threshold)为显著;abs规则:max模式下如果超过best+threshold为显著,min模式下如果低于best-threshold为显著;
    cooldown:触发一次条件后,等待一定epoch再进行检测,避免lr下降过速;
    min_lr:最小的允许lr;
    eps:如果新旧lr之间的差异小与1e-8,则忽略此次更新。

这里非常感谢facebook的员工给我们提供了如此多的选择与便利!
对于上述方法如有任何疑惑,还请查阅torch.optim文档


批归一化(Batch Normalization)

除了对学习率进行调整之外,Batch Normalization也可以有效地解决之前的问题。
我是在学习ResNet的时候第一次遇到批归一化这个概念的。随着深度神经网络深度的加深,训练越来越困难,收敛越来越慢。为此,很多论文都尝试解决这个问题,比如ReLU激活函数,再比如Residual Network,而BN本质上也是解释并从某个不同的角度来解决这个问题的。
通过使用Batch Normalization,我们可以加快网络的收敛速度,这样我们就可以使用较大的学习率来训练网络了。此外,BN还提高了网络的泛化能力。
BN的基本思想其实相当直观:
首先,因为深层神经网络在做非线性变换前的激活输入值(就是x=WU+B,U是输入)随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,之所以训练收敛慢,一般是整体分布逐渐往非线性函数的取值区间的上下限两端靠近(对于Sigmoid函数来说,意味着激活输入值WU+B是大的负值或正值),这就导致了反向传播时低层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因。
事实上,神经网络学习过程本质上是为了学习数据的分布,而BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0、方差为1的标准正态分布,其实就是把越来越偏的分布强制拉回比较标准的分布,这样使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,从而让梯度变大,避免梯度消失问题产生,而且梯度变大意味着学习收敛速度快,因此通过BN能大大加快训练速度。
下面来看看BN的具体操作过程:

即以下四个步骤:
1.计算样本均值。
2.计算样本方差。
3.对样本数据进行标准化处理。
4.进行平移和缩放处理。这里引入了γ和β两个参数。通过训练可学习重构的γ和β这两个参数,让我们的网络可以学习恢复出原始网络所要学习的特征分布。
下面是BN层的训练流程:

这里的详细过程如下:
输入:待进入激活函数的变量。
输出:
1.这里的K,在卷积网络中可以看作是卷积核个数,如网络中第n层有64个卷积核,就需要计算64次。

注意:在正向传播时,会使用γ与β使得BN层输出与输入一样。

2.在反向传播时利用γ与β求得梯度从而改变训练权值(变量)。
3.通过不断迭代直到训练结束,求得关于不同层的γ与β。
4.不断遍历训练集中的图片,取出每个batch_size中的γ与β,最后统计每层BN的γ与β各自的和除以图片数量得到平均值,并对其做无偏估计直作为每一层的E[x]与Var[x]。
5.在预测的正向传播时,对测试数据求取γ与β,并使用该层的E[x]与Var[x],通过图中11:所表示的公式计算BN层输出。

注意:在预测时,BN层的输出已经被改变,因此BN层在预测中的作用体现在此处。

上面输入的是待进入激活函数的变量,在残差网络ResNet中,的确也是先经过BN层再用relu函数做非线性处理的。那么,为什么BN层一般用在线性层和卷积层的后面,而不是放在非线性单元即激活函数之后呢?
因为非线性单元的输出分布形状会在训练过程中变化,归一化无法消除他的方差偏移。相反的,全连接和卷积层的输出一般是一个对称、非稀疏的一个分布,更加类似高斯分布,对他们进行归一化会产生更加稳定的分布。
比如,我们对一个高斯分布的数据relu激活,那么小于0的直接就被抑制了,这样得到的结果很难是高斯分布了,这时候再添加一个BN层就很难达到所需的效果。
很多实验证明,BatchNorm只要用了就有效果,所以在一般情况下没有理由不用。但也有相反的情况,比如当每个batch里所有的sample都非常相似的时候,相似到mean和variance都基本为0时,最好不要用BatchNorm。此外如果batch size为1,从原理上来讲,此时用BatchNorm是没有任何意义的。

注意:通常我们在进行Transfer Learning的时候,会冻结之前的网络权重,注意这时候往往也会冻结BatchNorm中训练好的moving averages值。这些moving averages值只适用于以前的旧的数据,对新数据不一定适用。所以最好的方法是在Transfer Learning的时候不要冻结BatchNorm层,让moving averages值重新从新的数据中学习。


批归一化实现

这里还是使用pytorch进行实现。
准备(对下列通用):

1
2
import torch
import torch.nn as nn

  1. 2d或3d输入

    1
    2
    3
    4
    5
    6
    # 添加了可学习的仿射变换参数
    m = nn.BatchNorm1d(100)
    # 未添加可学习的仿射变换参数
    m = nn.BatchNorm1d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100))
    output = m(input)

    我们查看m,可以看到有如下形式:

    1
    BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=False, track_running_stats=True)

    这里解释一下涉及到的参数:
    num_features:来自期望输入的特征数,该期望输入的大小为:batch_size * num_features(* width)
    eps:为保证数值稳定性(分母不能趋近或取0),给分母加上的值,默认为1e-5。
    momentum:计算动态均值和动态方差并进行移动平均所使用的动量,默认为0.1。
    affine:一个布尔值,当设为true时,就给该层添加可学习的仿射变换参数。仿射变换将在后文做简单介绍。
    BatchNorm1d可以有两种输入输出:
    1.输入(N,C),输出(N,C)。
    2.输入(N,C,L),输出(N,C,L)。

  2. 3d或4d输入

    1
    2
    3
    4
    5
    m = nn.BatchNorm2d(100)
    #或者
    m = nn.BatchNorm2d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100, 35, 45))
    output = m(input)

    BatchNorm2d也可以有两种输入输出:
    1.输入(N,C,L),输出(N,C,L)。
    2.输入(N,C,H,W),输出(N,C,H,W)。

  3. 4d或5d输入

    1
    2
    3
    m = nn.BatchNorm3d(100)
    #或者
    m = nn.BatchNorm3d(100, affine=False)

    BatchNorm3d同样支持两种输入输出:
    1.输入(N,C,H,W),输出(N,C,H,W)。
    2.输入(N,C,D,H,W),输出(N,C,D,H,W)。


仿射变换

这里我简单介绍一下仿射变换的概念,仿射变换(Affine Transformation或Affine Map)是一种二维坐标(x, y)到二维坐标(u, v)的变换,它是另外两种简单变换的叠加,一是线性变换,二是平移变换。同时,仿射变换保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点确定一个唯一的仿射变换。

补充:
共线性:若几个点变换前在一条线上,则仿射变换后仍然在一条线上。
平行性:若两条线变换前平行,则变换后仍然平行。
共线比例不变性:变换前一条线上两条线段的比例,在变换后比例不变。

在二维图像变换中,它的一般表达如下:

可以视为线性变换R和平移变换T的叠加。
另外,仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切。因此我们可以将几种简单的变换矩阵相乘来实现仿射变换。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>一段时间之前,在一个深度学习交流群里看到一个群友发问:为什么他的训练误差最后疯狂上下抖动而不是一直降低。<br><img src="/deep- + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>一段时间之前,在一个深度学习交流群里看到一个群友发问:为什么他的训练误差最后疯狂上下抖动而不是一直降低。<br><img src="/deep- @@ -1928,10 +1928,10 @@ - - + + @@ -1943,13 +1943,13 @@ 2019-10-01T05:05:31.000Z 2020-02-07T09:06:41.471Z -

之前在网上看到一个windows系统下的上帝模式,很好奇,尝试之后感觉不错,这里介绍一下创建的方法。除此之外附上一些类似的快捷操作。


上帝模式

上帝模式,即”God Mode”,或称为“完全控制面板”。它是windows系统中隐藏的一个简单的文件夹窗口,包含了几乎所有windows系统的设置,如控制面板的功能、界面个性化、辅助功能选项等控制设置,用户只需通过这一个窗口就能实现所有的操控,而不必再去为调整一个小小的系统设置细想半天究竟该在什么地方去打开设置。打开上帝模式后你将会看到如下界面:

好吧我承认和想象中的上帝模式不太一样,不过下面我还是介绍一下这个略显简陋的上帝模式是怎么设置的。

  1. 方式一:添加桌面快捷方式

    1. 首先在桌面新建一个文件夹。
    2. 将新建的文件夹命名为:GodMode.{ED7BA470-8E54-465E-825C-99712043E01C}
    3. 重命名完成后,你将看到一个类似于控制面板但没有名称的图标,双击打开,就可以看到之前所展示的上帝模式的界面了。
  2. 方式二:添加到快捷菜单

    1. win+R运行,输入regedit打开注册表编辑器,允许更改。
    2. 依次展开路径至HKEY_CLASSES_ROOT\DesktopBackground\Shell
    3. 点击shell后在右侧窗口鼠标右击,选择新建项。
    4. 把新建的项重命名为“上帝模式”。
    5. 点击上帝模式后,双击右侧窗口中的默认,在数值数据处输入“上帝模式”,点击确定。
    6. 右击上帝模式,选择新建项。
    7. 把新建的项重命名为“command”。
    8. 点击command后,双击右侧窗口中的默认,在数值数据处输入:explorer shell:::{ED7BA470-8E54-465E-825C-99712043E01C},确定。
    9. 这时候在桌面空白处右键打开快捷菜单,就可以看到上帝模式已成功添加。

类似的操作

在上面我的快捷菜单中,可以看到还有关机、重启、锁屏等选项。其实它们添加的操作和添加上帝模式的步骤是一样的,只需把命名为“上帝模式”的地方修改成“关机”等文字,并且在上文中的第8步中,用对应的数值数据即可。

这里提供四种功能对应的数值数据,其实这些和上面上帝模式的commmand命令都是可以直接在cmd中执行的:
关机Shutdown -s -f -t 00
注销Shutdown -l
重启Shutdown -r -f -t 00
锁屏Rundll32 User32.dll,LockWorkStation
事实上,锁屏功能可以直接使用win+L快捷键达到目的。除此之外,win还可以搭配其他的一些按键完成一些快捷操作,比如win+D可以快速最小化一切窗口回到桌面,想知道win有哪些搭配可以右键左下角的win图标查看。

]]>
+

之前在网上看到一个windows系统下的上帝模式,很好奇,尝试之后感觉不错,这里介绍一下创建的方法。除此之外附上一些类似的快捷操作。


上帝模式

上帝模式,即”God Mode”,或称为“完全控制面板”。它是windows系统中隐藏的一个简单的文件夹窗口,包含了几乎所有windows系统的设置,如控制面板的功能、界面个性化、辅助功能选项等控制设置,用户只需通过这一个窗口就能实现所有的操控,而不必再去为调整一个小小的系统设置细想半天究竟该在什么地方去打开设置。打开上帝模式后你将会看到如下界面:

好吧我承认和想象中的上帝模式不太一样,不过下面我还是介绍一下这个略显简陋的上帝模式是怎么设置的。

  1. 方式一:添加桌面快捷方式

    1. 首先在桌面新建一个文件夹。
    2. 将新建的文件夹命名为:GodMode.{ED7BA470-8E54-465E-825C-99712043E01C}
    3. 重命名完成后,你将看到一个类似于控制面板但没有名称的图标,双击打开,就可以看到之前所展示的上帝模式的界面了。
  2. 方式二:添加到快捷菜单

    1. win+R运行,输入regedit打开注册表编辑器,允许更改。
    2. 依次展开路径至HKEY_CLASSES_ROOT\DesktopBackground\Shell
    3. 点击shell后在右侧窗口鼠标右击,选择新建项。
    4. 把新建的项重命名为“上帝模式”。
    5. 点击上帝模式后,双击右侧窗口中的默认,在数值数据处输入“上帝模式”,点击确定。
    6. 右击上帝模式,选择新建项。
    7. 把新建的项重命名为“command”。
    8. 点击command后,双击右侧窗口中的默认,在数值数据处输入:explorer shell:::{ED7BA470-8E54-465E-825C-99712043E01C},确定。
    9. 这时候在桌面空白处右键打开快捷菜单,就可以看到上帝模式已成功添加。

类似的操作

在上面我的快捷菜单中,可以看到还有关机、重启、锁屏等选项。其实它们添加的操作和添加上帝模式的步骤是一样的,只需把命名为“上帝模式”的地方修改成“关机”等文字,并且在上文中的第8步中,用对应的数值数据即可。

这里提供四种功能对应的数值数据,其实这些和上面上帝模式的commmand命令都是可以直接在cmd中执行的:
关机Shutdown -s -f -t 00
注销Shutdown -l
重启Shutdown -r -f -t 00
锁屏Rundll32 User32.dll,LockWorkStation
事实上,锁屏功能可以直接使用win+L快捷键达到目的。除此之外,win还可以搭配其他的一些按键完成一些快捷操作,比如win+D可以快速最小化一切窗口回到桌面,想知道win有哪些搭配可以右键左下角的win图标查看。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>之前在网上看到一个windows系统下的上帝模式,很好奇,尝试之后感觉不错,这里介绍一下创建的方法。除此之外附上一些类似的快捷操作。</p><h + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前在网上看到一个windows系统下的上帝模式,很好奇,尝试之后感觉不错,这里介绍一下创建的方法。除此之外附上一些类似的快捷操作。</p><h @@ -1973,13 +1973,13 @@ 2019-10-01T02:45:38.000Z 2020-01-30T04:22:50.807Z -

本文介绍在模型评估可能会出现的过拟合与欠拟合两种现象,并对解决方法做一个总结。


解释

我们先通过图片来直观地解释这两种现象:

在上图中,右边是过拟合的情况,它指的是模型对于训练数据拟合过度,反映到评估指标上,就是模型在训练集上的表现很好,但在测试集和新数据上的表现较差。这是因为在这种条件下,模型过于复杂,导致把噪声数据的特征也学习到了模型中,导致模型的泛化能力下降,从而在后期的应用过程中很容易输出错误的预测结果。
左边是欠拟合的情况,它指的是在训练和预测时的表现都不好,这样的模型没有很好地捕捉到数据地特征,从而不能够很好地拟合数据。
相比而言,中间是拟合适当的情况,这种模型在应用中就具有很好的鲁棒性。


解决方法

  1. 针对过拟合

    1. 获取更多数据

      更多的样本可以让模型学到更多有效的特征,从而减小噪声的影响。
      当然,一般情况下直接增加数据是很困难的,因此我们需要通过一定的规则来扩充训练数据。比如,在图像分类问题上,我们可以使用数据增强的方法,通过对图像的平移、旋转、缩放等方式来扩充数据;更进一步地,可以使用生成式对抗网络来合成大量新的训练数据。
    2. 降低模型复杂度

      模型复杂度过高是数据量较小时过拟合的主要原因。适当降低模型的复杂度可以避免模型拟合过多的噪声。比如,在神经网络模型中减少网络层数、神经元个数等;在决策树模型中降低树的深度、进行剪枝等。

      注意:网络深度增加引起的准确率退化不一定是过拟合引起的,这是因为深度造成的梯度消失、梯度爆炸等问题,这在ResNet的论文中有讨论,详细可以看我的博文deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现

    3. 正则化方法

      这里的方法主要是权重正则化法,具体说明可以参考machine-learning笔记:机器学习中正则化的理解
    4. 交叉验证

      交叉验证包括简单交叉验证(数据丰富时)、S折交叉验证(最常用)和留一交叉验证(数据匮乏时)。
    5. 集成学习

      即把多个模型集成在一起,从而降低单一模型的过拟合风险。主要有Bagging(bootstrap aggregating)和Boosting(adaptive boosting)这两种集成学习方法。
  2. 针对欠拟合

    解决欠拟合问题也可以参照解决过拟合问题的思路;
    1. 添加新特征

      当特征不足或者现有特征与样本标签的相关性不强时,模型容易出现欠拟合。
      因此,通过挖掘“上下文特征”、“组合特征”等新的特征,往往能够取得更好的效果。
      在深度学习中,也有很多模型可以帮助完成特征工程,比如因此分解机、梯度提升决策树、Deep-crossing等都可以成为丰富特征的方法。
    2. 增加模型复杂度

      当模型过于简单时,增加模型复杂度可以使模型拥有更强的拟合能力。比如,在线性模型中添加高次项,在神经网络模型中增加网络层数、神经元个数等。
      对于模型的选择,我在文末补充了两种模型选择的准则供参考。
    3. 减小正则化系数

      正则化是用来防止过拟合的,但当模型出现欠拟合现象时,我们就应该有针对性地减小正则化系数。

模型选择准则

模型选择的信息准则有很多,我这里介绍我知道的两个比较常用的模型选择准则:

  1. AIC准则

    赤池信息准则(Akaike Information Criterion,AIC)公式定义如下:其中k表示模型参数个数(复杂度),L表示经验误差(似然函数)。
    当需要从一组可供选择的模型中选择最佳模型时,通常选择AIC最小的模型。
  2. BIC准则

    贝叶斯信息准则(Bayesian Information Criterion,BIC)是对AIC准则的改进,定义如下:与AIC不同,这里k的系数不再是常数。其中n代表的是样本量(数据量),这样,BIC准则就与样本量相关了。当样本量足够时,过拟合的风险变小,我们就可以允许模型复杂一些。
    这里再次附上这张直观的图片,方便理解与体会。简析可参考machine-learning笔记:机器学习中正则化的理解
]]>
+

本文介绍在模型评估可能会出现的过拟合与欠拟合两种现象,并对解决方法做一个总结。


解释

我们先通过图片来直观地解释这两种现象:

在上图中,右边是过拟合的情况,它指的是模型对于训练数据拟合过度,反映到评估指标上,就是模型在训练集上的表现很好,但在测试集和新数据上的表现较差。这是因为在这种条件下,模型过于复杂,导致把噪声数据的特征也学习到了模型中,导致模型的泛化能力下降,从而在后期的应用过程中很容易输出错误的预测结果。
左边是欠拟合的情况,它指的是在训练和预测时的表现都不好,这样的模型没有很好地捕捉到数据地特征,从而不能够很好地拟合数据。
相比而言,中间是拟合适当的情况,这种模型在应用中就具有很好的鲁棒性。


解决方法

  1. 针对过拟合

    1. 获取更多数据

      更多的样本可以让模型学到更多有效的特征,从而减小噪声的影响。
      当然,一般情况下直接增加数据是很困难的,因此我们需要通过一定的规则来扩充训练数据。比如,在图像分类问题上,我们可以使用数据增强的方法,通过对图像的平移、旋转、缩放等方式来扩充数据;更进一步地,可以使用生成式对抗网络来合成大量新的训练数据。
    2. 降低模型复杂度

      模型复杂度过高是数据量较小时过拟合的主要原因。适当降低模型的复杂度可以避免模型拟合过多的噪声。比如,在神经网络模型中减少网络层数、神经元个数等;在决策树模型中降低树的深度、进行剪枝等。

      注意:网络深度增加引起的准确率退化不一定是过拟合引起的,这是因为深度造成的梯度消失、梯度爆炸等问题,这在ResNet的论文中有讨论,详细可以看我的博文deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现

    3. 正则化方法

      这里的方法主要是权重正则化法,具体说明可以参考machine-learning笔记:机器学习中正则化的理解
    4. 交叉验证

      交叉验证包括简单交叉验证(数据丰富时)、S折交叉验证(最常用)和留一交叉验证(数据匮乏时)。
    5. 集成学习

      即把多个模型集成在一起,从而降低单一模型的过拟合风险。主要有Bagging(bootstrap aggregating)和Boosting(adaptive boosting)这两种集成学习方法。
  2. 针对欠拟合

    解决欠拟合问题也可以参照解决过拟合问题的思路;
    1. 添加新特征

      当特征不足或者现有特征与样本标签的相关性不强时,模型容易出现欠拟合。
      因此,通过挖掘“上下文特征”、“组合特征”等新的特征,往往能够取得更好的效果。
      在深度学习中,也有很多模型可以帮助完成特征工程,比如因此分解机、梯度提升决策树、Deep-crossing等都可以成为丰富特征的方法。
    2. 增加模型复杂度

      当模型过于简单时,增加模型复杂度可以使模型拥有更强的拟合能力。比如,在线性模型中添加高次项,在神经网络模型中增加网络层数、神经元个数等。
      对于模型的选择,我在文末补充了两种模型选择的准则供参考。
    3. 减小正则化系数

      正则化是用来防止过拟合的,但当模型出现欠拟合现象时,我们就应该有针对性地减小正则化系数。

模型选择准则

模型选择的信息准则有很多,我这里介绍我知道的两个比较常用的模型选择准则:

  1. AIC准则

    赤池信息准则(Akaike Information Criterion,AIC)公式定义如下:其中k表示模型参数个数(复杂度),L表示经验误差(似然函数)。
    当需要从一组可供选择的模型中选择最佳模型时,通常选择AIC最小的模型。
  2. BIC准则

    贝叶斯信息准则(Bayesian Information Criterion,BIC)是对AIC准则的改进,定义如下:与AIC不同,这里k的系数不再是常数。其中n代表的是样本量(数据量),这样,BIC准则就与样本量相关了。当样本量足够时,过拟合的风险变小,我们就可以允许模型复杂一些。
    这里再次附上这张直观的图片,方便理解与体会。简析可参考machine-learning笔记:机器学习中正则化的理解
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>本文介绍在模型评估可能会出现的过拟合与欠拟合两种现象,并对解决方法做一个总结。</p><hr><h1 id="解释"><a href="#解释" + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>本文介绍在模型评估可能会出现的过拟合与欠拟合两种现象,并对解决方法做一个总结。</p><hr><h1 id="解释"><a href="#解释" @@ -2001,13 +2001,13 @@ 2019-10-01T02:13:34.000Z 2020-02-15T23:14:23.438Z -

这是我在一个相关的群里看到的一个论文,这篇论文比较新,看完之后觉得对目前AI发展状况的了解有一定价值,就放了上来。


论文

这里直接提供图片形式的原文:


其他

难得有一篇以AI开篇的文章,由于在我不到一年前真正接触AI相关知识时,一直疑惑人工智能、机器学习与深度学习之间的关系。直到看了台大教授李宏毅的课才知道三者之间的包含关系,这里就把课件中的一张图片放上来,一目了然:

最后,再补张和deep-learning笔记:一篇非常经典的论文——NatureDeepReview文末对应的一张我觉得挺真实的图哈哈。

不得不说,目前丰富的库和各种深度学习框架的确极大地方便了AI的学习与研究,许多轮子都已造好。学会运用这些工具还是很有帮助的!

]]>
+

这是我在一个相关的群里看到的一个论文,这篇论文比较新,看完之后觉得对目前AI发展状况的了解有一定价值,就放了上来。


论文

这里直接提供图片形式的原文:


其他

难得有一篇以AI开篇的文章,由于在我不到一年前真正接触AI相关知识时,一直疑惑人工智能、机器学习与深度学习之间的关系。直到看了台大教授李宏毅的课才知道三者之间的包含关系,这里就把课件中的一张图片放上来,一目了然:

最后,再补张和deep-learning笔记:一篇非常经典的论文——NatureDeepReview文末对应的一张我觉得挺真实的图哈哈。

不得不说,目前丰富的库和各种深度学习框架的确极大地方便了AI的学习与研究,许多轮子都已造好。学会运用这些工具还是很有帮助的!

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>这是我在一个相关的群里看到的一个论文,这篇论文比较新,看完之后觉得对目前AI发展状况的了解有一定价值,就放了上来。</p><hr><h1 id= + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>这是我在一个相关的群里看到的一个论文,这篇论文比较新,看完之后觉得对目前AI发展状况的了解有一定价值,就放了上来。</p><hr><h1 id= @@ -2031,13 +2031,13 @@ 2019-10-01T01:34:28.000Z 2020-01-29T05:05:39.621Z -

在学习机器学习理论的过程中,支持向量机(SVM)应该是我们会遇到的第一个对数学要求比较高的概念。理解它的原理要花费了我不少时间,写这篇博文是因为我之前看到的一个有关SVM的问题,其解答需用到SVM的相关数学原理,可以促使我思考。支持向量机的具体原理以及推导网上有大量资源,我也会在文中提供一些供参考。


简介

支持向量机是一种有监督的学习方法,主要思想是建立一个最优决策超平面,使得该平面两侧距离该平面最近的两类样本之间的距离最大化,从而对分类问题提供良好的泛化能力。
这里有个小故事,也是我第一次看SVM课程时老师提到的,可以通过这个小故事大致理解一下SVM在做什么。

它的优点主要有如下四点:
(1)相对于其他训练分类算法,SVM不需要过多的样本。
(2)SVM引入了核函数,可以处理高维的样本。
(3)结构风险最小。也就是说,分类器对问题真实模型的逼近与真实解之间的累计误差最小。
(4)由于SVM的非线性,它擅长应付线性不可分的问题。这主要是用松弛变量(惩罚变量)和核函数来实现的。
这里我附上我所知的三个SVM的常用软件工具包:SVMLightLibSVMLiblinear


问题

下面就是我在文章开头提到的问题,直接搬运:

解析中提到的拉格朗日乘子法和KKT条件,也是我在看到这个问题后才尝试去理解的。能力有限,不能自己很好的解释,这里附上瑞典皇家理工学院(KTH)“统计学习基础”课程的KKT课件,个人觉得讲的很直观且详细了。

]]>
+

在学习机器学习理论的过程中,支持向量机(SVM)应该是我们会遇到的第一个对数学要求比较高的概念。理解它的原理要花费了我不少时间,写这篇博文是因为我之前看到的一个有关SVM的问题,其解答需用到SVM的相关数学原理,可以促使我思考。支持向量机的具体原理以及推导网上有大量资源,我也会在文中提供一些供参考。


简介

支持向量机是一种有监督的学习方法,主要思想是建立一个最优决策超平面,使得该平面两侧距离该平面最近的两类样本之间的距离最大化,从而对分类问题提供良好的泛化能力。
这里有个小故事,也是我第一次看SVM课程时老师提到的,可以通过这个小故事大致理解一下SVM在做什么。

它的优点主要有如下四点:
(1)相对于其他训练分类算法,SVM不需要过多的样本。
(2)SVM引入了核函数,可以处理高维的样本。
(3)结构风险最小。也就是说,分类器对问题真实模型的逼近与真实解之间的累计误差最小。
(4)由于SVM的非线性,它擅长应付线性不可分的问题。这主要是用松弛变量(惩罚变量)和核函数来实现的。
这里我附上我所知的三个SVM的常用软件工具包:SVMLightLibSVMLiblinear


问题

下面就是我在文章开头提到的问题,直接搬运:

解析中提到的拉格朗日乘子法和KKT条件,也是我在看到这个问题后才尝试去理解的。能力有限,不能自己很好的解释,这里附上瑞典皇家理工学院(KTH)“统计学习基础”课程的KKT课件,个人觉得讲的很直观且详细了。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>在学习机器学习理论的过程中,支持向量机(SVM)应该是我们会遇到的第一个对数学要求比较高的概念。理解它的原理要花费了我不少时间,写这篇博文是因为 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在学习机器学习理论的过程中,支持向量机(SVM)应该是我们会遇到的第一个对数学要求比较高的概念。理解它的原理要花费了我不少时间,写这篇博文是因为 @@ -2057,13 +2057,13 @@ 2019-09-21T11:58:40.000Z 2020-01-19T00:23:28.718Z -

今天终于完成了计算机三级数据库的考试,这也是本学期的第一门考试。听说计算机三级中要属计算机网络最简单,然而出于学到更多有用的知识的目的,我报了数据库。然而事实证明也没学到多少,毕竟这个计算机等级考试是给非计算机专业的人设置的,现在只求能过。不过两三天书看下来,还是有些收获,现在考完了有时间就在这里记一下,方便自己和别人今后有需要看。

References

电子文献:
https://blog.csdn.net/he626shidizai/article/details/90707037
https://blog.csdn.net/u013011841/article/details/39023859


范式

注意:本文中的范式指的是数据库范式。

在设计数据库时,为了设计一个良好的逻辑关系,必须要使关系受一定条件的约束,这种约束逐渐成为一种规范,就是我们所说的范式。
目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF)。要求最低的是1NF,往后依次变得严格。其中最后的5NF又称完美范式。
数据库一般只需满足3NF,下面我就介绍一下前三种范式。

第一范式

数据库考试官方教程并没有对每个范式的定义进行讲解,另外因为文字定义比较晦涩难懂,我这里通过多方参考,用图片的形式来展示各个约束条件。
首先,1NF是所有关系型数据库最基本的要求,它的定义为:符合1NF的关系中的每个属性都不可再分。下图就是一个违反1NF的例子:

修改如下:

上面的情况就符合1NF了。
我们还可以把第一范式分成两点来理解:

  1. 每个字段都只能存放单一值

    还是上反例: 上图中,第一行的课程有两个值,这就不符合第一范式了。因此要修改成这样:
  2. 每笔记录都要能用一个唯一的主键识别

    这里出现了重复组,同样也不满足1NF,因为缺乏唯一的标识码。因此修改如下:

第二范式

第二范式是建立在第一范式的基础上的,它的改进在于:消除了非主属性对于码的部分函数依赖。
第二范式消除了非主属性对于码的部分函数依赖,也就是说,第二范式中所有非主属性完全依赖于主键,即不能依赖于主键的一部分属性。
为了解释明白,还是通过实例的说明:

上表中,学号和课程号组合在一起是主键,但是姓名只由学号决定,这就违反了第二范式。同样的,课程名只由课程号决定,这也违反了第二范式。此外,只需要知道学号和课程号就能知道成绩。
为了满足第二范式,我们就需要对上表做如下拆分:

第三范式

同样的,第三范式建立在第二范式的基础上。不同之处在于,在第二范式的基础之上,第三范式中非主属性都不传递依赖于主键。
这是什么意思?还是看图说话:

上表中,主键是学号,且已满足第二范式。然而,学校的地址也可以根据学校名称来确定,第三范式就是在这里再做一个分解:

]]>
+

今天终于完成了计算机三级数据库的考试,这也是本学期的第一门考试。听说计算机三级中要属计算机网络最简单,然而出于学到更多有用的知识的目的,我报了数据库。然而事实证明也没学到多少,毕竟这个计算机等级考试是给非计算机专业的人设置的,现在只求能过。不过两三天书看下来,还是有些收获,现在考完了有时间就在这里记一下,方便自己和别人今后有需要看。

References

电子文献:
https://blog.csdn.net/he626shidizai/article/details/90707037
https://blog.csdn.net/u013011841/article/details/39023859


范式

注意:本文中的范式指的是数据库范式。

在设计数据库时,为了设计一个良好的逻辑关系,必须要使关系受一定条件的约束,这种约束逐渐成为一种规范,就是我们所说的范式。
目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF)。要求最低的是1NF,往后依次变得严格。其中最后的5NF又称完美范式。
数据库一般只需满足3NF,下面我就介绍一下前三种范式。

第一范式

数据库考试官方教程并没有对每个范式的定义进行讲解,另外因为文字定义比较晦涩难懂,我这里通过多方参考,用图片的形式来展示各个约束条件。
首先,1NF是所有关系型数据库最基本的要求,它的定义为:符合1NF的关系中的每个属性都不可再分。下图就是一个违反1NF的例子:

修改如下:

上面的情况就符合1NF了。
我们还可以把第一范式分成两点来理解:

  1. 每个字段都只能存放单一值

    还是上反例: 上图中,第一行的课程有两个值,这就不符合第一范式了。因此要修改成这样:
  2. 每笔记录都要能用一个唯一的主键识别

    这里出现了重复组,同样也不满足1NF,因为缺乏唯一的标识码。因此修改如下:

第二范式

第二范式是建立在第一范式的基础上的,它的改进在于:消除了非主属性对于码的部分函数依赖。
第二范式消除了非主属性对于码的部分函数依赖,也就是说,第二范式中所有非主属性完全依赖于主键,即不能依赖于主键的一部分属性。
为了解释明白,还是通过实例的说明:

上表中,学号和课程号组合在一起是主键,但是姓名只由学号决定,这就违反了第二范式。同样的,课程名只由课程号决定,这也违反了第二范式。此外,只需要知道学号和课程号就能知道成绩。
为了满足第二范式,我们就需要对上表做如下拆分:

第三范式

同样的,第三范式建立在第二范式的基础上。不同之处在于,在第二范式的基础之上,第三范式中非主属性都不传递依赖于主键。
这是什么意思?还是看图说话:

上表中,主键是学号,且已满足第二范式。然而,学校的地址也可以根据学校名称来确定,第三范式就是在这里再做一个分解:

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>今天终于完成了计算机三级数据库的考试,这也是本学期的第一门考试。听说计算机三级中要属计算机网络最简单,然而出于学到更多有用的知识的目的,我报了数 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>今天终于完成了计算机三级数据库的考试,这也是本学期的第一门考试。听说计算机三级中要属计算机网络最简单,然而出于学到更多有用的知识的目的,我报了数 @@ -2083,13 +2083,13 @@ 2019-09-17T00:56:49.000Z 2020-02-07T09:02:49.446Z -

之前一直在win10下使用hexo搭建部署博客,方法参见:hexo笔记:开始创建个人博客——方法及原因。那么,如果想在linux环境下使用hexo,该如何操作呢?

References

电子文献:
https://blog.csdn.net/y5492853/article/details/79529410


原因

由于在更改主题配置文件_config.yml时,长达八百多行的配置文件总是让我找得头晕目眩。由于VScode(好像)没有提供字符的快速查找匹配功能,我之前一直采用一种笨拙的办法,即在文件的空处输入想要查找的字符然后左键选中,这个时候文件中同样的字符也会被选中,这样快速拉动滚动条时就可以比较明显地发现想找的目标字符了。
然而对于这种办法,我觉得主要有两大问题:

  1. 忘记删除在空白处添加的文字

    我就犯过这样低级的错误,找到并更改之后没有删除自己添加的字符就直接快乐地ctrl+S了,于是就造成了网站能打开但是一片空白的bug。所以大家没事还是不要随意在主题配置文件中添加文字。
  2. 不是长久之计

    虽然这个八百多行的文件已经让我够呛了,然后或许今后还会遇到更长的文件,那么这种方法就会变得极其低效(而且伤眼睛)。
    基于这些因素,我脑子里的第一个反映就是vim编辑器中的对文件字符的查找定位的功能(关于vim的使用,等我多多尝试并熟练之后再做小结)。
    好了,接下来就开始操作吧。

更正

最近突然发现VScode自带了搜索功能,可以直接在整个文件夹中搜索关键词。这里所给的快捷键是ctrl+shift+F,但win10用户可能会发现按了之后没有任何反应。事实上,反应还是有的,当你再次打字时,就会发现简体变成了繁体,再次按ctrl+shift+F即可恢复。

直接点击搜索图标即可便捷地进行搜索,为我之前眼瞎没有发现表示无奈,但下面还是写一下怎么安装。


安装

首先安装node.js。这里就没windows下直接双击exe安装包那么easy啦,打开终端,老老实实输命令:

1
2
3
sudo apt-get install nodejs
sudo apt install nodejs-legacy
sudo apt install npm

其实熟练之后觉得apt是真的好用。
由于ubuntu源中的node.js是旧版本,下面会出现问题,我在后文解释。
由于npm服务器在国外可能会影响下载速度,和windows下的步骤一样,我们换成淘宝镜像:

1
sudo npm config set registry https://registry.npm.taobao.org

这时候如果我们直接安装hexo,会出现如下错误:

因此,我们安装node升级工具n:

1
sudo npm install n -g

并且使用sudo n stable升级版本,若看到如下输出,说明升级成功:

注意:fetch可能需要花费一点时间,这时候终端不会有任何输出,不要以为出错了,耐心等待即可,不要ctrl+C中止。

最后,我们安装hexo:

1
sudo npm install -g hexo

注:-g表示安装到全局环境。

接下来的初始化操作跟windows下基本一样,可以参照我之前的博文。我继续对原来的博客进行编辑,所以无需初始化一个新的,直接把windows的对应文件夹整个copy过来就行了。

]]>
+

之前一直在win10下使用hexo搭建部署博客,方法参见:hexo笔记:开始创建个人博客——方法及原因。那么,如果想在linux环境下使用hexo,该如何操作呢?

References

电子文献:
https://blog.csdn.net/y5492853/article/details/79529410


原因

由于在更改主题配置文件_config.yml时,长达八百多行的配置文件总是让我找得头晕目眩。由于VScode(好像)没有提供字符的快速查找匹配功能,我之前一直采用一种笨拙的办法,即在文件的空处输入想要查找的字符然后左键选中,这个时候文件中同样的字符也会被选中,这样快速拉动滚动条时就可以比较明显地发现想找的目标字符了。
然而对于这种办法,我觉得主要有两大问题:

  1. 忘记删除在空白处添加的文字

    我就犯过这样低级的错误,找到并更改之后没有删除自己添加的字符就直接快乐地ctrl+S了,于是就造成了网站能打开但是一片空白的bug。所以大家没事还是不要随意在主题配置文件中添加文字。
  2. 不是长久之计

    虽然这个八百多行的文件已经让我够呛了,然后或许今后还会遇到更长的文件,那么这种方法就会变得极其低效(而且伤眼睛)。
    基于这些因素,我脑子里的第一个反映就是vim编辑器中的对文件字符的查找定位的功能(关于vim的使用,等我多多尝试并熟练之后再做小结)。
    好了,接下来就开始操作吧。

更正

最近突然发现VScode自带了搜索功能,可以直接在整个文件夹中搜索关键词。这里所给的快捷键是ctrl+shift+F,但win10用户可能会发现按了之后没有任何反应。事实上,反应还是有的,当你再次打字时,就会发现简体变成了繁体,再次按ctrl+shift+F即可恢复。

直接点击搜索图标即可便捷地进行搜索,为我之前眼瞎没有发现表示无奈,但下面还是写一下怎么安装。


安装

首先安装node.js。这里就没windows下直接双击exe安装包那么easy啦,打开终端,老老实实输命令:

1
2
3
sudo apt-get install nodejs
sudo apt install nodejs-legacy
sudo apt install npm

其实熟练之后觉得apt是真的好用。
由于ubuntu源中的node.js是旧版本,下面会出现问题,我在后文解释。
由于npm服务器在国外可能会影响下载速度,和windows下的步骤一样,我们换成淘宝镜像:

1
sudo npm config set registry https://registry.npm.taobao.org

这时候如果我们直接安装hexo,会出现如下错误:

因此,我们安装node升级工具n:

1
sudo npm install n -g

并且使用sudo n stable升级版本,若看到如下输出,说明升级成功:

注意:fetch可能需要花费一点时间,这时候终端不会有任何输出,不要以为出错了,耐心等待即可,不要ctrl+C中止。

最后,我们安装hexo:

1
sudo npm install -g hexo

注:-g表示安装到全局环境。

接下来的初始化操作跟windows下基本一样,可以参照我之前的博文。我继续对原来的博客进行编辑,所以无需初始化一个新的,直接把windows的对应文件夹整个copy过来就行了。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>之前一直在win10下使用hexo搭建部署博客,方法参见:<a href="https://gsy00517.github.io/hexo201 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>之前一直在win10下使用hexo搭建部署博客,方法参见:<a href="https://gsy00517.github.io/hexo201 @@ -2113,13 +2113,13 @@ 2019-09-15T07:03:39.000Z 2020-02-15T00:09:47.436Z -

在接触了一些ML的知识后,大家一定会对正则化这个词不陌生,但是我感觉根据这个词的字面意思不能够直接地理解它的概念。因此我打算写一篇文章做个记录,方便以后回忆。

References

参考文献:
[1]统计学习方法(第2版)


线性代数中的正则化

如果直接搜索正则化这个名词,首先得到的一般是代数几何中的一个概念。
百度词条对它的解释是:给平面不可约代数曲线以某种形式的全纯参数表示。
怎么样?是不是觉得一头雾水。
这里我推荐使用谷歌或者维基百科来查询这些专业名词。

对于不能科学上网的朋友,没关系,我这里提供了谷歌镜像wikiwand,大家可以在上面得到一样的搜索结果。

我们直接到维基百科搜索regularization:
里面第一段是这样解释的:In mathematics, statistics, and computer science, particularly in machine learning and inverse problems, regularization is the process of adding information in order to solve an ill-posed problem or to prevent overfitting.
这就和我们在机器学习应用中的目的比较相近了。


机器学习中的正则化

在机器学习中,正则化是一种为了减小测试误差的行为(有时候会增加训练误差)。
我们在构造机器学习模型时,最终目的是让模型在面对新数据的时候,可以有很好的表现。当你用比较复杂的模型比如神经网络去拟合数据时,很容易出现过拟合现象(训练集表现很好,测试集表现较差),这会导致模型的泛化能力下降,这时候,我们就需要使用正则化,来降低模型的复杂度。
为了加深印象,我下面简单介绍几种常用的机器学习正则化方法:

  1. 早停法(Early Stopping)

    早停法,就是当训练集的误差变好,但是验证集的误差变坏(即泛化效果变差)的时候停止训练。这种方法可以一定程度上有效地防止过拟合,同时这也说明了验证集在机器学习中的重要性。
  2. 权重正则化法

    因为噪声相比于正常信号而言,通常会在某些点出现较大的峰值。所以,只要我们保证权重系数在绝对值意义上足够小,就能够保证噪声不会被过度响应,这也是奥卡姆剃刀原理的表现,即模型不应过度复杂,尤其是当数据量不大的时候。

    上面是在一个网课上看到的、我觉得可以较好地呈现模型的复杂度与数据量对模型预测表现的影响的一张图片,其中向左的横轴表示数据量大小,向右的横轴表示模型复杂度,竖轴是预测表现。通过这张图,可以很明显地观察到:模型的复杂度提升需要大量的数据作为依托。
    权重正则化主要有两种:

    • L1正则:$ J=J_{0}+\lambda \left | w \right |_{1} $,其中J代表损失函数(也称代价函数),$ \left | w \right |_{1} $代表参数向量w的L1范数。
    • L2正则(weight decay):$ J=J_{0}+\lambda \left | w \right |_{2} $,其中$ \left | w \right |_{2} $代表参数向量w的L2范数。 这里就产生了Lasso回归岭回归两大机器学习经典算法。其中Lasso回归是一种压缩估计,可以通过构造惩罚函数得到一个较为精炼的模型,使得它可以压缩一些系数,同时设定一些系数为0,从而达到特征选择的目的。基于Lasso回归这种可以选择特征并降维的特性,它主要有这些适用情况:
    1. 样本量比较小,但指标量非常多的时候(易于过拟合)。
    2. 进行高维统计时。
    3. 需要对特征进行选择时。
      对于这些回归的详细解释,大家可以到网上搜集相关信息。

      补充:
      L0范数:向量中非零元素的个数。
      L1范数:向量中每个元素绝对值的和。
      L2范数:向量元素绝对值的平方和再开方。

    下面我再附上一组图,希望能帮助更好地理解权重正则化:
    首先我们可视化一个损失函数。

    下面我们看一看正则化项的图示,这里使用L1范数作为正则化项。

    接着,我们将上面两者线性组合:

    我们来看看结果:

    可见,正则化项的引入排除了大量原本属于最优解的点,上图的情况中剩下一个唯一的局部最优解。
    正则化项的引入,除了符合奥卡姆剃刀原理之外。同时从贝叶斯估计的角度看,正则化项对应于模型的先验概率,即相当于假设复杂的模型具有较小的先验概率,而简单的模型具有较大的先验概率。

  3. 数据增强

    数据增强可以丰富图像数据集,有效防止过拟合。这种方法在AlexNet中有很好的应用,大家可以看看我的博文deep-learning笔记:开启深度学习热潮——AlexNet
  4. 随机失活(dropout)

    dropout即随机砍掉一部分神经元之间的连接,每次只更新一部分,这可以有效地增加它的鲁棒性,提高泛化能力。这个方法在AlexNet中也有详细的解释,推荐大家去看一下。 以上就是比较常规且流行的正则化方式,今后或许会有补充,也欢迎大家提供意见~

奥卡姆剃刀原理

上文在正则化一节提到了奥卡姆剃刀原理,这里就简单做个说明。
奥卡姆剃刀原理应用于模型选择时可以简单表述为如下思想:在所有可能选择的模型中,能够很好地解释已知数据并且十分简单的模型才是最好的模型,也就是我们应该选择的模型。

]]>
+

在接触了一些ML的知识后,大家一定会对正则化这个词不陌生,但是我感觉根据这个词的字面意思不能够直接地理解它的概念。因此我打算写一篇文章做个记录,方便以后回忆。

References

参考文献:
[1]统计学习方法(第2版)


线性代数中的正则化

如果直接搜索正则化这个名词,首先得到的一般是代数几何中的一个概念。
百度词条对它的解释是:给平面不可约代数曲线以某种形式的全纯参数表示。
怎么样?是不是觉得一头雾水。
这里我推荐使用谷歌或者维基百科来查询这些专业名词。

对于不能科学上网的朋友,没关系,我这里提供了谷歌镜像wikiwand,大家可以在上面得到一样的搜索结果。

我们直接到维基百科搜索regularization:
里面第一段是这样解释的:In mathematics, statistics, and computer science, particularly in machine learning and inverse problems, regularization is the process of adding information in order to solve an ill-posed problem or to prevent overfitting.
这就和我们在机器学习应用中的目的比较相近了。


机器学习中的正则化

在机器学习中,正则化是一种为了减小测试误差的行为(有时候会增加训练误差)。
我们在构造机器学习模型时,最终目的是让模型在面对新数据的时候,可以有很好的表现。当你用比较复杂的模型比如神经网络去拟合数据时,很容易出现过拟合现象(训练集表现很好,测试集表现较差),这会导致模型的泛化能力下降,这时候,我们就需要使用正则化,来降低模型的复杂度。
为了加深印象,我下面简单介绍几种常用的机器学习正则化方法:

  1. 早停法(Early Stopping)

    早停法,就是当训练集的误差变好,但是验证集的误差变坏(即泛化效果变差)的时候停止训练。这种方法可以一定程度上有效地防止过拟合,同时这也说明了验证集在机器学习中的重要性。
  2. 权重正则化法

    因为噪声相比于正常信号而言,通常会在某些点出现较大的峰值。所以,只要我们保证权重系数在绝对值意义上足够小,就能够保证噪声不会被过度响应,这也是奥卡姆剃刀原理的表现,即模型不应过度复杂,尤其是当数据量不大的时候。

    上面是在一个网课上看到的、我觉得可以较好地呈现模型的复杂度与数据量对模型预测表现的影响的一张图片,其中向左的横轴表示数据量大小,向右的横轴表示模型复杂度,竖轴是预测表现。通过这张图,可以很明显地观察到:模型的复杂度提升需要大量的数据作为依托。
    权重正则化主要有两种:

    • L1正则:$ J=J_{0}+\lambda \left | w \right |_{1} $,其中J代表损失函数(也称代价函数),$ \left | w \right |_{1} $代表参数向量w的L1范数。
    • L2正则(weight decay):$ J=J_{0}+\lambda \left | w \right |_{2} $,其中$ \left | w \right |_{2} $代表参数向量w的L2范数。 这里就产生了Lasso回归岭回归两大机器学习经典算法。其中Lasso回归是一种压缩估计,可以通过构造惩罚函数得到一个较为精炼的模型,使得它可以压缩一些系数,同时设定一些系数为0,从而达到特征选择的目的。基于Lasso回归这种可以选择特征并降维的特性,它主要有这些适用情况:
    1. 样本量比较小,但指标量非常多的时候(易于过拟合)。
    2. 进行高维统计时。
    3. 需要对特征进行选择时。
      对于这些回归的详细解释,大家可以到网上搜集相关信息。

      补充:
      L0范数:向量中非零元素的个数。
      L1范数:向量中每个元素绝对值的和。
      L2范数:向量元素绝对值的平方和再开方。

    下面我再附上一组图,希望能帮助更好地理解权重正则化:
    首先我们可视化一个损失函数。

    下面我们看一看正则化项的图示,这里使用L1范数作为正则化项。

    接着,我们将上面两者线性组合:

    我们来看看结果:

    可见,正则化项的引入排除了大量原本属于最优解的点,上图的情况中剩下一个唯一的局部最优解。
    正则化项的引入,除了符合奥卡姆剃刀原理之外。同时从贝叶斯估计的角度看,正则化项对应于模型的先验概率,即相当于假设复杂的模型具有较小的先验概率,而简单的模型具有较大的先验概率。

  3. 数据增强

    数据增强可以丰富图像数据集,有效防止过拟合。这种方法在AlexNet中有很好的应用,大家可以看看我的博文deep-learning笔记:开启深度学习热潮——AlexNet
  4. 随机失活(dropout)

    dropout即随机砍掉一部分神经元之间的连接,每次只更新一部分,这可以有效地增加它的鲁棒性,提高泛化能力。这个方法在AlexNet中也有详细的解释,推荐大家去看一下。 以上就是比较常规且流行的正则化方式,今后或许会有补充,也欢迎大家提供意见~

奥卡姆剃刀原理

上文在正则化一节提到了奥卡姆剃刀原理,这里就简单做个说明。
奥卡姆剃刀原理应用于模型选择时可以简单表述为如下思想:在所有可能选择的模型中,能够很好地解释已知数据并且十分简单的模型才是最好的模型,也就是我们应该选择的模型。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>在接触了一些ML的知识后,大家一定会对正则化这个词不陌生,但是我感觉根据这个词的字面意思不能够直接地理解它的概念。因此我打算写一篇文章做个记录, + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在接触了一些ML的知识后,大家一定会对正则化这个词不陌生,但是我感觉根据这个词的字面意思不能够直接地理解它的概念。因此我打算写一篇文章做个记录, @@ -2139,13 +2139,13 @@ 2019-09-15T03:38:59.000Z 2020-02-15T14:09:31.093Z -

继之前那篇deep-learning笔记:着眼于深度——VGG简介与pytorch实现,我觉得还是有必要提一下VGG的前辈——具有历史意义的AlexNet,于是就写了这篇文章简要介绍一下。

References

电子文献:
https://blog.csdn.net/zym19941119/article/details/78982441

参考文献:
[1]ImageNet Classification with Deep Convolutional Neural Networks


简介

ALexNet是第一个运用大型深度卷积神经网络的模型,在ILSVRC中一下子比前一年把错误率降低了10%,这是非常惊人的,也很快引起了注意。于是,自2012年开始,深度学习热潮由此引发。

根据我之前听网课的笔记以及网上的其他文章,我把AlexNet主要的进步归纳如下:

  1. 使用大型深度卷积神经网络。
  2. 分组卷积(groupconvolution)来充分利用GPU。
  3. 随机失活dropout:一种有效的正则化方法。关于正则化,可以看我的博文machine-learning笔记:机器学习中正则化的理解
  4. 数据增强data augumentation:增大数据集以减小过拟合问题。
  5. ReLU激活函数:即max(0,x),至今还被广泛应用。

个人思考

这段时间也看了不少东西,对于如何提升神经网络的性能这个问题,我觉得主要有如下三个方面:

  1. 从网络本身入手

    1. 增加深度。
    2. 增加宽度。
    3. 减少参数量。
    4. 防止过拟合。
    5. 解决梯度消失的问题。
  2. 从数据集入手

    1. 尽可能使用多的数据。
  3. 从硬件入手

    1. 提升GPU性能。
    2. 充分利用现有的GPU性能。
      当你阅读完AlexNet的论文,你会发现它在这几个方面都有思考且做出了非常优秀的改进。

论文

在放论文之前,我还是先贴一张流程图,方便在阅读论文的时候进行对照与理解。

下面奉上宝贵的论文:
论文原版
论文中文版
从introduction第一句开始,作者就开始了一段长长的吐槽:
Current approaches to object recognition make essential use of machine learning methods…
吐槽Yann LeCun大佬的论文被顶会拒收了仅仅因为Yann LeCun使用了神经网络。其实,那段时间之前,由于SVM等机器学习方法的兴起,神经网络是一种被许多ML大佬们看不起的算法模型。
在introduction的最后,作者留下了这样一句经典的话:
All of our experiments suggest that our results can be improved simply by waiting for faster GPUs and bigger datasets to become available.
有没有感觉到一种新世界大门被打开的感觉呢?
有关论文别的内容,我暂不多说了,大家可以自己看论文学习与体会。
附上推荐重点阅读的章节:3.1 ReLU Nonlinearity;3.5 Overall Architecture;4 Reducing Overfitting。


说明

同我写VGG的那篇文章中一样,我在英文原版中用黄颜色高亮了我觉得重要的内容给自己和大家今后参考。
另外,我在这里推荐大家还是先尝试阅读英文原版。一方面由于一些公式、符号以及名词的原因,英文原版叙述更精准,中文翻译有缺漏、偏颇之处;另一方面更重要的,接触这些方面的知识仅参考中文是远远不够的。
在这里我推荐一个chrome英文pdf阅读插件,大家可以自己到chrome里面搜索安装:


有了这个插件,遇到不认识的单词,只需双击单词,就可以看到中文释义,一定程度上可以保证阅读的流畅性。但是如果想从根本上解决问题,只有好好背单词吧(我也在朝这个方向努力…)。
另外,iPad的上也有好多强大的app,在这里不一一推荐了。

补充:最近又发现一款特别好用且美观的查词插件,功能非常强大,推荐一下:沙拉查词

]]>
+

继之前那篇deep-learning笔记:着眼于深度——VGG简介与pytorch实现,我觉得还是有必要提一下VGG的前辈——具有历史意义的AlexNet,于是就写了这篇文章简要介绍一下。

References

电子文献:
https://blog.csdn.net/zym19941119/article/details/78982441

参考文献:
[1]ImageNet Classification with Deep Convolutional Neural Networks


简介

ALexNet是第一个运用大型深度卷积神经网络的模型,在ILSVRC中一下子比前一年把错误率降低了10%,这是非常惊人的,也很快引起了注意。于是,自2012年开始,深度学习热潮由此引发。

根据我之前听网课的笔记以及网上的其他文章,我把AlexNet主要的进步归纳如下:

  1. 使用大型深度卷积神经网络。
  2. 分组卷积(groupconvolution)来充分利用GPU。
  3. 随机失活dropout:一种有效的正则化方法。关于正则化,可以看我的博文machine-learning笔记:机器学习中正则化的理解
  4. 数据增强data augumentation:增大数据集以减小过拟合问题。
  5. ReLU激活函数:即max(0,x),至今还被广泛应用。

个人思考

这段时间也看了不少东西,对于如何提升神经网络的性能这个问题,我觉得主要有如下三个方面:

  1. 从网络本身入手

    1. 增加深度。
    2. 增加宽度。
    3. 减少参数量。
    4. 防止过拟合。
    5. 解决梯度消失的问题。
  2. 从数据集入手

    1. 尽可能使用多的数据。
  3. 从硬件入手

    1. 提升GPU性能。
    2. 充分利用现有的GPU性能。
      当你阅读完AlexNet的论文,你会发现它在这几个方面都有思考且做出了非常优秀的改进。

论文

在放论文之前,我还是先贴一张流程图,方便在阅读论文的时候进行对照与理解。

下面奉上宝贵的论文:
论文原版
论文中文版
从introduction第一句开始,作者就开始了一段长长的吐槽:
Current approaches to object recognition make essential use of machine learning methods…
吐槽Yann LeCun大佬的论文被顶会拒收了仅仅因为Yann LeCun使用了神经网络。其实,那段时间之前,由于SVM等机器学习方法的兴起,神经网络是一种被许多ML大佬们看不起的算法模型。
在introduction的最后,作者留下了这样一句经典的话:
All of our experiments suggest that our results can be improved simply by waiting for faster GPUs and bigger datasets to become available.
有没有感觉到一种新世界大门被打开的感觉呢?
有关论文别的内容,我暂不多说了,大家可以自己看论文学习与体会。
附上推荐重点阅读的章节:3.1 ReLU Nonlinearity;3.5 Overall Architecture;4 Reducing Overfitting。


说明

同我写VGG的那篇文章中一样,我在英文原版中用黄颜色高亮了我觉得重要的内容给自己和大家今后参考。
另外,我在这里推荐大家还是先尝试阅读英文原版。一方面由于一些公式、符号以及名词的原因,英文原版叙述更精准,中文翻译有缺漏、偏颇之处;另一方面更重要的,接触这些方面的知识仅参考中文是远远不够的。
在这里我推荐一个chrome英文pdf阅读插件,大家可以自己到chrome里面搜索安装:


有了这个插件,遇到不认识的单词,只需双击单词,就可以看到中文释义,一定程度上可以保证阅读的流畅性。但是如果想从根本上解决问题,只有好好背单词吧(我也在朝这个方向努力…)。
另外,iPad的上也有好多强大的app,在这里不一一推荐了。

补充:最近又发现一款特别好用且美观的查词插件,功能非常强大,推荐一下:沙拉查词

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>继之前那篇<a href="https://gsy00517.github.io/deep-learning20190915073809/" t + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>继之前那篇<a href="https://gsy00517.github.io/deep-learning20190915073809/" t @@ -2169,13 +2169,13 @@ 2019-09-15T01:56:28.000Z 2020-02-15T00:09:03.260Z -

在上一篇文章deep-learning笔记:着眼于深度——VGG简介与pytorch实现中,我用到了markdown其他的一些使用方法,因此我想在此对之前的一篇文章markdown笔记:markdown的基本使用做一些补充。

References

电子文献:
https://www.jianshu.com/p/25f0139637b7
https://www.jianshu.com/p/fd97e1f8f699
https://www.jianshu.com/p/68e6f82d88b7
https://www.jianshu.com/p/7c02c112d532


公式插入

无论是学习ML还是DL,我们总是离不开数学的,于是利用markdown插入数学公式就成了一个的需求。那么怎么在markdown中插入公式呢?
markdown中的公式分为两类,即行内公式与行间公式。它们对应的代码如下:

1
2
$ \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. $
$$\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.$$

让我们来看一下效果:
行内公式:$ \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. $
行间公式:

如果你是使用hexo编写博客,那么默认的设置是无法转义markdown公式的,解决这个问题的配置方法可以参考本文顶部给出的第三个链接。
另外要注意,在使用公式时,对应文件需开启mathjax选项。

补充更新:在查看next主题配置文件时,我注意到next好像自带mathjax支持,设置如下,这样就无需在每个文件中添加开启mathjax的选项。

markdown公式的具体语法可以参照本文的第一个链接,你可以在typora中根据它的官方文档进行尝试。

注意:在typora中,只需输入$或者$$就可直接进入公式编辑,无需输入一对。

有机会我再对上面提到的语法进行搬运。下面介绍一种更简单省力的方法(也是我在用的方法):

  1. 打开在线LaTex公式编辑器
  2. 在上方的框框中输入你想要的公式: 你可以在下方的GIF图中随时观察你的输入时候符合预期,如在书写word文档等类似文本时需要插入公式,也可以直接复制图片。
  3. 拷贝下方黄颜色方框中的代码到markdown文件。 你可以选择去掉两边的“\”和方括号,否则你的公式两侧将会套有方括号,另外你还需要使用上文提到的$来确定公式显示方式。
    这里我们这样输入:$ x+y=z $
    得到:$ x+y=z $。
    以上就是使用LaTex给markdown添加公式的方法。
    你也可以使用黄颜色框中的URL选项来添加代码,格式是![](URL)
    例如,输入:![](https://latex.codecogs.com/gif.latex?x&plus;y=z)
    可以看到:
    这种方法就不需要文章顶部链接三中的配置了,也是一种推荐的方法。

    注:若在在线LaTex公式编辑器中找不到需要的元素或者符号的话,可以看一看LaTex常用公式整理


代码高亮

markdown中使代码高亮的格式如下:

1
2
3
三个反引号+语言名
代码...
三个反引号

例如,输入:

可以看到:

1
print("hello world!")

同样的,在typora中,你也不必输入成对的三个反引号。
这里我要提醒一个我以前用Rmarkdown时踩过的坑:

注意!他俩是不一样的!

真正的“`”在这里:

]]>
+

在上一篇文章deep-learning笔记:着眼于深度——VGG简介与pytorch实现中,我用到了markdown其他的一些使用方法,因此我想在此对之前的一篇文章markdown笔记:markdown的基本使用做一些补充。

References

电子文献:
https://www.jianshu.com/p/25f0139637b7
https://www.jianshu.com/p/fd97e1f8f699
https://www.jianshu.com/p/68e6f82d88b7
https://www.jianshu.com/p/7c02c112d532


公式插入

无论是学习ML还是DL,我们总是离不开数学的,于是利用markdown插入数学公式就成了一个的需求。那么怎么在markdown中插入公式呢?
markdown中的公式分为两类,即行内公式与行间公式。它们对应的代码如下:

1
2
$ \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. $
$$\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.$$

让我们来看一下效果:
行内公式:$ \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. $
行间公式:

如果你是使用hexo编写博客,那么默认的设置是无法转义markdown公式的,解决这个问题的配置方法可以参考本文顶部给出的第三个链接。
另外要注意,在使用公式时,对应文件需开启mathjax选项。

补充更新:在查看next主题配置文件时,我注意到next好像自带mathjax支持,设置如下,这样就无需在每个文件中添加开启mathjax的选项。

markdown公式的具体语法可以参照本文的第一个链接,你可以在typora中根据它的官方文档进行尝试。

注意:在typora中,只需输入$或者$$就可直接进入公式编辑,无需输入一对。

有机会我再对上面提到的语法进行搬运。下面介绍一种更简单省力的方法(也是我在用的方法):

  1. 打开在线LaTex公式编辑器
  2. 在上方的框框中输入你想要的公式: 你可以在下方的GIF图中随时观察你的输入时候符合预期,如在书写word文档等类似文本时需要插入公式,也可以直接复制图片。
  3. 拷贝下方黄颜色方框中的代码到markdown文件。 你可以选择去掉两边的“\”和方括号,否则你的公式两侧将会套有方括号,另外你还需要使用上文提到的$来确定公式显示方式。
    这里我们这样输入:$ x+y=z $
    得到:$ x+y=z $。
    以上就是使用LaTex给markdown添加公式的方法。
    你也可以使用黄颜色框中的URL选项来添加代码,格式是![](URL)
    例如,输入:![](https://latex.codecogs.com/gif.latex?x&plus;y=z)
    可以看到:
    这种方法就不需要文章顶部链接三中的配置了,也是一种推荐的方法。

    注:若在在线LaTex公式编辑器中找不到需要的元素或者符号的话,可以看一看LaTex常用公式整理


代码高亮

markdown中使代码高亮的格式如下:

1
2
3
三个反引号+语言名
代码...
三个反引号

例如,输入:

可以看到:

1
print("hello world!")

同样的,在typora中,你也不必输入成对的三个反引号。
这里我要提醒一个我以前用Rmarkdown时踩过的坑:

注意!他俩是不一样的!

真正的“`”在这里:

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>在上一篇文章<a href="https://gsy00517.github.io/deep-learning20190915073809/" + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>在上一篇文章<a href="https://gsy00517.github.io/deep-learning20190915073809/" @@ -2195,13 +2195,13 @@ 2019-09-14T23:38:09.000Z 2020-02-15T14:10:08.541Z -

VGG是我第一个自己编程实践的卷积神经网络,也是挺高兴的,下面我就对VGG在这篇文章中做一个分享。

References

电子文献:
https://blog.csdn.net/xiaohuihui1994/article/details/89207534
https://blog.csdn.net/sinat_33487968/article/details/83584289
https://blog.csdn.net/qq_32172681/article/details/95971492

参考文献:
[1]Very Deep Convolutional Networks For Large-scale Image Recognition


简介

VGG模型在2014年取得了ILSVRC竞赛的第二名,第一名是GoogLeNet。但是VGG在多个迁移学习任务中的表现要优于GoogleNet。

相比之前的神经网络,VGG主要有两大进步:其一是它增加了深度,其二是它使用了小的3x3的卷积核,这可以使它在增加深度的时候一定程度上防止了参数的增长。缺点是它的参数量比较庞大,但这并不意味着它不值得我们仔细研究。下图展示的是VGG的结构。

为了通过对比来对VGG的一些改进进行解释,VGG的作者在论文中提供了多个版本。


论文

要详细分析VGG,我可能不能像网上写的那样好,更不可能像论文一样明白。那么我在这里就先附上论文。
论文原版
论文中文版
我在英文原版中用黄颜色高亮了我觉得比较重要的内容,大家可以参考一下。
大家也可以自己到网上进行搜索,这类经典的网络网上有许多介绍与分析。
通过论文或者网上的资源对这个网络有一定理解之后,你可以看看我下面的代码实现。


结论

此篇论文的得出了一些结论,总结如下:

  1. 在一定范围内,通过增加网络深度能有效地提升网络性能。这在ResNet里更是得到了显著地体现,上面的ILSVRC历年winner表现统计图就是一个很好的证明,可参见deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现
  2. 与AlexNet对比可知,多个小卷积核比单个大卷积核性能要好。
  3. AlexNet中用到的LRN层(局部响应归一化层)并没有带来性能的提升,因此可以排除。
  4. 尺度抖动(scale jittering)即多尺度训练、多尺度测试有利于网络性能的提升。
  5. 最佳模型为VGG16,其从头到尾只用了3x3的卷积和2x2的池化。

特点

VGG的特点(创新点)主要有如下四个:

  1. 小卷积核

    VGG使用多个小卷积核来代替大的,这样一方面可以减少参数,另一方面相当于进行了更多的非线性映射,可以增加网络的拟合、表达能力。
  2. 小池化核

    相比AlexNet的3x3池化核,VGG一律采用了2x2的池化核。
  3. 层数更深

    若仅计算conv、fc层的话,VGG中常用的网络层数达到了16、19层(VGG16效果最好),这相较于前几年的研究是一个深度的提升。
  4. conv替代fc

    在基本的CNN中,全连接层的作用是将经过多个卷积层和池化层的图像特征图中的特征进行整合,获取图像特征具有的高层含义,用于图像分类。
    如果我们把全连接层的输出不再看成n个节点的集合,而是视作一个1x1xn的输出层,那么我们就可以用卷积层来替换全连接层了。并且从数学角度看,它和全连接层是一样的。
    因为卷积层没有全连接层对输入的限制,因此使用卷积层代替全连接层可以接收任意宽或高的输入。
    此外,相对于全连接层而言,使用卷积层不会破坏图像的空间结构。这也是一些的网络使用1x1的全卷积层代替全连接层的重要原因。

感受野

这里会涉及一个名为感受野(Receptive Field)的概念,它指的是卷积神经网络每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域大小。简而言之,就是特征图上的一点跟原有图上有关系的点的区域。一般取一个pixel为单位,而输入的感受野就是1即只对应其自身的那个像素。画图易知,两层3x3的卷积层所得到的感受野与一层5x5的卷积层的感受野相同,这也是VGG使用3x3小卷积核来代替的原理之一。
感受野是CNN中一个比较重要的概念,一些目标检测的流行算法如SSD、Faster Rcnn等中的prior box和anchor box的设计都是以感受野为依据的。可以看一下computer-vision笔记:anchor-box


1x1卷积核

虽然VGG所用的是2x2的卷积核,但是在上文提到了一些网络使用1x1的全卷积层代替全连接层,那么顺便就对1x1卷积核的作用做一个总结。

  1. 如上文所述,使用卷积层就没有全连接层对输入尺寸的限制,这也方便了许多。
  2. 全连接层会改变网络的空间结构,卷积层不会破坏图像的空间结构。一般要学习是相邻的边界等特征,而全连接毫无目的地将整张图“全连接”,换言之全连接输出的是一维向量,势必将丢失大量二维信息,这显然是不合适的。
  3. 可以用于为决策增加非线性因素。
  4. 一些模型用1x1的全连接层来调整网络维度。比如MobileNet使用1x1的卷积核来扩维,GoogleNet、ResNet使用1x1的卷积核来降维。这里的降维类似于压缩处理,并不会影响训练结果,而1x1的卷积核可以使网络变薄,可以成倍地减少计算量。如下图所示,如果我们使用naive的inception,那么最后concatenate出来的特征图厚度会很大,而添加上1x1的卷积核之后就可以调整厚度了。

自己实现

这里我使用pytorch框架来实现VGG。pytorch是一个相对较新的框架,但热度上升很快。根据网上的介绍,pytorch是一个非常适合于学习与科研的深度学习框架。我尝试了之后,也发现上手很快。
在pytorch中,神经网络可以通过torch.nn包来构建。这里我不一一介绍了,大家可以参考pytorch官方中文教程来学习,照着文档自己动手敲一遍之后,基本上就知道了pytorch如何使用了。
为了方便直观的理解,我先提供一个VGG16版本的流程图。

下面是我实现VGG19版本的代码:
首先,我们import所需的包。

1
2
import torch
import torch.nn as nn

接下来,我们定义神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class VGG(nn.Module):
def __init__(self, num_classes = 1000): #imagenet图像库总共1000个类
super(VGG, self).__init__() #先运行父类nn.Module初始化函数

self.conv1_1 = nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 3, padding = 1)
#定义图像卷积函数:输入为图像(3个频道,即RGB图),输出为64张特征图,卷积核为3x3正方形,为保留原空间分辨率,卷积层的空间填充为1即padding等于1,也就是防止每次卷积尺寸缩小过快导致无法使用更多的卷积层
self.conv1_2 = nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, padding = 1)

self.conv2_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, padding = 1)
self.conv2_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, padding = 1)

self.conv3_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_3 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_4 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)

self.conv4_1 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_3 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_4 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)

self.conv5_1 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_3 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_4 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)

self.relu = nn.ReLU(inplace = True) #inplace=TRUE表示原地操作
self.max = nn.MaxPool2d(kernel_size = 2, stride = 2)

self.fc1 = nn.Linear(512 * 7 * 7, 4096) #定义全连接函数1为线性函数:y = Wx + b,并将512*7*7个节点连接到4096个节点上
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, num_classes)
#定义全连接函数3为线性函数:y = Wx + b,并将4096个节点连接到num_classes个节点上,然后可用softmax进行处理

#定义该神经网络的向前传播函数,该函数必须定义,一旦定义成功,向后传播函数也会自动生成(autograd)
def forward(self, x):

x = self.relu(self.conv1_1(x))
x = self.relu(self.conv1_2(x))
x = self.max(x)
#输入x经过卷积之后,经过激活函数ReLU,循环两次,最后使用2x2的窗口进行最大池化Max pooling,然后更新到x

x = self.relu(self.conv2_1(x))
x = self.relu(self.conv2_2(x))
x = self.max(x)

x = self.relu(self.conv3_1(x))
x = self.relu(self.conv3_2(x))
x = self.relu(self.conv3_3(x))
x = self.relu(self.conv3_4(x))
x = self.max(x)

x = self.relu(self.conv4_1(x))
x = self.relu(self.conv4_2(x))
x = self.relu(self.conv4_3(x))
x = self.relu(self.conv4_4(x))
x = self.max(x)

x = self.relu(self.conv5_1(x))
x = self.relu(self.conv5_2(x))
x = self.relu(self.conv5_3(x))
x = self.relu(self.conv5_4(x))
x = self.max(x)

x = x.view(-1, self.num_flat_features(x)) #view函数将张量x变形成一维的向量形式,总特征数并不改变,为接下来的全连接作准备

x = self.fc1(x) #输入x经过全连接1,然后更新x
x = self.fc2(x)
x = self.fc3(x)
return x

def num_flat_features(self, x):
size = x.size()[1:] #all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features

vgg = VGG()
print(vgg)

我们print网络,可以看到输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VGG(
(conv1_1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv1_2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2_1): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2_2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_1): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_4): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_1): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_4): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_4): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(relu): ReLU(inplace=True)
(max): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(fc1): Linear(in_features=25088, out_features=4096, bias=True)
(fc2): Linear(in_features=4096, out_features=4096, bias=True)
(fc3): Linear(in_features=4096, out_features=1000, bias=True)
)

最后我们随机生成一个张量来进行验证。

1
2
3
input = torch.randn(1, 3, 224, 224)
out = vgg(input)
print(out)

其中(1, 3, 224, 224)表示1个3x224x224的矩阵,这是因为VGG输入的是固定尺寸的224x224的RGB(三通道)图像。
如果没有报错,那么就说明你的神经网络成功运行通过了。
我们也可以使用torch.nn.functional来实现激活函数与池化层,这样的话,你需要还需要多引入一个包:

1
2
3
import torch
import torch.nn as nn
import torch.nn.functional as F #新增

同时,你不需要在init中实例化激活函数与最大池化层,相应的,你需要对forward前馈函数进行更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def forward(self, x):

x = F.relu(self.conv1_1(x))
x = F.relu(self.conv1_2(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)
#输入x经过卷积之后,经过激活函数ReLU,循环两次,最后使用2x2的窗口进行最大池化Max pooling,然后更新到x

x = F.relu(self.conv2_1(x))
x = F.relu(self.conv2_2(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv3_1(x))
x = F.relu(self.conv3_2(x))
x = F.relu(self.conv3_3(x))
x = F.relu(self.conv3_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv4_1(x))
x = F.relu(self.conv4_2(x))
x = F.relu(self.conv4_3(x))
x = F.relu(self.conv4_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv5_1(x))
x = F.relu(self.conv5_2(x))
x = F.relu(self.conv5_3(x))
x = F.relu(self.conv5_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = x.view(-1, self.num_flat_features(x)) #view函数将张量x变形成一维的向量形式,总特征数并不改变,为接下来的全连接作准备

x = self.fc1(x) #输入x经过全连接1,然后更新x
x = self.fc2(x)
x = self.fc3(x)
return x

如果你之前运行通过的话,那么这里也是没有问题的。
这里我想说明一下torch.nntorch.nn.functional的区别。
这两个包中有许多类似的激活函数与损失函数,但是它们又有如下不同:
首先,在定义函数层(继承nn.Module)时,init函数中应该用torch.nn,例如torch.nn.ReLUtorch.nn.Dropout2d,而forward中应该用torch.nn.functional,例如torch.nn.functional.relu,不过请注意,init里面定义的是标准的网络层。只有torch.nn定义的才会进行训练。torch.nn.functional定义的需要自己手动设置参数。所以通常,激活函数或者卷积之类的都用torch.nn定义。
另外,torch.nn是类,必须要先在init中实例化,然后在forward中使用,而torch.nn.functional可以直接在forward中使用。
大家还可以通过官方文档torch.nn.functionaltorch.nn来进一步了解两者的区别。
大家或许发现,我的代码中有大量的重复性工作。是的,你将在文章后面的官方实现中看到优化的代码,但是相对来说,我的代码更加直观些,完全是按照网络的结构顺序从上到下编写的,可以方便初学者(including myself)的理解。


出现的问题

虽然我的代码比较简单直白,但是过程中并不是一帆风顺的,出现了两次报错:

  1. 输入输出不匹配

    当我第一遍运行时,出现了一个RuntimeError: 这是一个超级低级的错误,经学长提醒后我才发现,我两个卷积层之间输出输入的channel数并不匹配: 唉又是ctrl+C+V惹的祸,改正后的网络可以参见上文。
    在这里,我想提醒我自己和大家注意一下卷积层输入输出的维度公式:
    假设输入的宽、高记为W、H。
    超参数中,卷积核的维度是F,stride步长是S,padding是P。
    那么输出的宽X与高Y可用如下公式表示:然而,当我在计算ResNet的维度的时候,发现套用这个公式是除不尽的。于是我搜索到了如下规则:
    1. 对卷积层操作,除不尽时,向下取整。
    2. 对池化层操作,除不尽时,向上取整。
  2. 没有把张量转化成一维向量

    上面的问题解决了,结果还有错误:

    根据报错,可以发现3584x7是等于25088的,结合pytorch官方文档,我意识到我在把张量输入全连接层时,没有把它拍扁成一维。因此,我按照官方文档添加了如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    x = x.view(-1, self.num_flat_features(x))

    def num_flat_features(self, x):
    size = x.size()[1:] #all dimensions except the batch dimension
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    再运行,问题解决。
    注意这里的“except the batch dimension”,我在后面deep-learning笔记:记首次ResNet实战中就踩了坑。

另外,我原本写的代码中,在卷积层之间的对应位置都加上了relu激活函数与池化层。后来我才意识到,由于它们不具有任何需要学习的参数,我可以直接把它们拿出来单独定义:

1
2
self.relu = nn.ReLU(inplace = True)
self.max = nn.MaxPool2d(kernel_size = 2, stride = 2)

虽然是一些很低级的坑,但我还是想写下来供我自己和大家今后参考。


官方源码

由于VGG的结构设计非常有规律,因此官方源码给出了更简洁的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch.nn as nn
import math

class VGG(nn.Module):

def __init__(self, features, num_classes=1000, init_weights=True):
super(VGG, self).__init__()
self.features = features
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
if init_weights:
self._initialize_weights()

def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x

def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
m.weight.data.normal_(0, 0.01)
m.bias.data.zero_()

因为VGG中卷积层的重复性比较高,所以官方使用一个函数来循环产生卷积层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def make_layers(cfg, batch_norm=False):
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)

接下来定义各个版本的卷积层(可参考上文中对论文的截图),这里的“M”表示的是最大池化层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
cfg = {
'A': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'B': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'E': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

def vgg11(**kwargs):
model = VGG(make_layers(cfg['A']), **kwargs)
return model

def vgg11_bn(**kwargs):
model = VGG(make_layers(cfg['A'], batch_norm=True), **kwargs)
return model

def vgg13(**kwargs):
model = VGG(make_layers(cfg['B']), **kwargs)
return model

def vgg13_bn(**kwargs):
model = VGG(make_layers(cfg['B'], batch_norm=True), **kwargs)
return model

def vgg16(**kwargs):
model = VGG(make_layers(cfg['D']), **kwargs)
return model

def vgg16_bn(**kwargs):
model = VGG(make_layers(cfg['D'], batch_norm=True), **kwargs)
return model

def vgg19(**kwargs):
model = VGG(make_layers(cfg['E']), **kwargs)
return model

def vgg19_bn(**kwargs):
model = VGG(make_layers(cfg['E'], batch_norm=True), **kwargs)
return model

if __name__ == '__main__':
# 'VGG', 'vgg11', 'vgg11_bn', 'vgg13', 'vgg13_bn', 'vgg16', 'vgg16_bn', 'vgg19_bn', 'vgg19'
# Example
net11 = vgg11()
print(net11)

附上pytorch官方源码链接。可以在vision/torchvision/models/下找到一系列用pytorch实现的经典神经网络模型。
好了,以上就是VGG的介绍与实现,如有不足之处欢迎大家补充!

]]>
+

VGG是我第一个自己编程实践的卷积神经网络,也是挺高兴的,下面我就对VGG在这篇文章中做一个分享。

References

电子文献:
https://blog.csdn.net/xiaohuihui1994/article/details/89207534
https://blog.csdn.net/sinat_33487968/article/details/83584289
https://blog.csdn.net/qq_32172681/article/details/95971492

参考文献:
[1]Very Deep Convolutional Networks For Large-scale Image Recognition


简介

VGG模型在2014年取得了ILSVRC竞赛的第二名,第一名是GoogLeNet。但是VGG在多个迁移学习任务中的表现要优于GoogleNet。

相比之前的神经网络,VGG主要有两大进步:其一是它增加了深度,其二是它使用了小的3x3的卷积核,这可以使它在增加深度的时候一定程度上防止了参数的增长。缺点是它的参数量比较庞大,但这并不意味着它不值得我们仔细研究。下图展示的是VGG的结构。

为了通过对比来对VGG的一些改进进行解释,VGG的作者在论文中提供了多个版本。


论文

要详细分析VGG,我可能不能像网上写的那样好,更不可能像论文一样明白。那么我在这里就先附上论文。
论文原版
论文中文版
我在英文原版中用黄颜色高亮了我觉得比较重要的内容,大家可以参考一下。
大家也可以自己到网上进行搜索,这类经典的网络网上有许多介绍与分析。
通过论文或者网上的资源对这个网络有一定理解之后,你可以看看我下面的代码实现。


结论

此篇论文的得出了一些结论,总结如下:

  1. 在一定范围内,通过增加网络深度能有效地提升网络性能。这在ResNet里更是得到了显著地体现,上面的ILSVRC历年winner表现统计图就是一个很好的证明,可参见deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现
  2. 与AlexNet对比可知,多个小卷积核比单个大卷积核性能要好。
  3. AlexNet中用到的LRN层(局部响应归一化层)并没有带来性能的提升,因此可以排除。
  4. 尺度抖动(scale jittering)即多尺度训练、多尺度测试有利于网络性能的提升。
  5. 最佳模型为VGG16,其从头到尾只用了3x3的卷积和2x2的池化。

特点

VGG的特点(创新点)主要有如下四个:

  1. 小卷积核

    VGG使用多个小卷积核来代替大的,这样一方面可以减少参数,另一方面相当于进行了更多的非线性映射,可以增加网络的拟合、表达能力。
  2. 小池化核

    相比AlexNet的3x3池化核,VGG一律采用了2x2的池化核。
  3. 层数更深

    若仅计算conv、fc层的话,VGG中常用的网络层数达到了16、19层(VGG16效果最好),这相较于前几年的研究是一个深度的提升。
  4. conv替代fc

    在基本的CNN中,全连接层的作用是将经过多个卷积层和池化层的图像特征图中的特征进行整合,获取图像特征具有的高层含义,用于图像分类。
    如果我们把全连接层的输出不再看成n个节点的集合,而是视作一个1x1xn的输出层,那么我们就可以用卷积层来替换全连接层了。并且从数学角度看,它和全连接层是一样的。
    因为卷积层没有全连接层对输入的限制,因此使用卷积层代替全连接层可以接收任意宽或高的输入。
    此外,相对于全连接层而言,使用卷积层不会破坏图像的空间结构。这也是一些的网络使用1x1的全卷积层代替全连接层的重要原因。

感受野

这里会涉及一个名为感受野(Receptive Field)的概念,它指的是卷积神经网络每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域大小。简而言之,就是特征图上的一点跟原有图上有关系的点的区域。一般取一个pixel为单位,而输入的感受野就是1即只对应其自身的那个像素。画图易知,两层3x3的卷积层所得到的感受野与一层5x5的卷积层的感受野相同,这也是VGG使用3x3小卷积核来代替的原理之一。
感受野是CNN中一个比较重要的概念,一些目标检测的流行算法如SSD、Faster Rcnn等中的prior box和anchor box的设计都是以感受野为依据的。可以看一下computer-vision笔记:anchor-box


1x1卷积核

虽然VGG所用的是2x2的卷积核,但是在上文提到了一些网络使用1x1的全卷积层代替全连接层,那么顺便就对1x1卷积核的作用做一个总结。

  1. 如上文所述,使用卷积层就没有全连接层对输入尺寸的限制,这也方便了许多。
  2. 全连接层会改变网络的空间结构,卷积层不会破坏图像的空间结构。一般要学习是相邻的边界等特征,而全连接毫无目的地将整张图“全连接”,换言之全连接输出的是一维向量,势必将丢失大量二维信息,这显然是不合适的。
  3. 可以用于为决策增加非线性因素。
  4. 一些模型用1x1的全连接层来调整网络维度。比如MobileNet使用1x1的卷积核来扩维,GoogleNet、ResNet使用1x1的卷积核来降维。这里的降维类似于压缩处理,并不会影响训练结果,而1x1的卷积核可以使网络变薄,可以成倍地减少计算量。如下图所示,如果我们使用naive的inception,那么最后concatenate出来的特征图厚度会很大,而添加上1x1的卷积核之后就可以调整厚度了。

自己实现

这里我使用pytorch框架来实现VGG。pytorch是一个相对较新的框架,但热度上升很快。根据网上的介绍,pytorch是一个非常适合于学习与科研的深度学习框架。我尝试了之后,也发现上手很快。
在pytorch中,神经网络可以通过torch.nn包来构建。这里我不一一介绍了,大家可以参考pytorch官方中文教程来学习,照着文档自己动手敲一遍之后,基本上就知道了pytorch如何使用了。
为了方便直观的理解,我先提供一个VGG16版本的流程图。

下面是我实现VGG19版本的代码:
首先,我们import所需的包。

1
2
import torch
import torch.nn as nn

接下来,我们定义神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class VGG(nn.Module):
def __init__(self, num_classes = 1000): #imagenet图像库总共1000个类
super(VGG, self).__init__() #先运行父类nn.Module初始化函数

self.conv1_1 = nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 3, padding = 1)
#定义图像卷积函数:输入为图像(3个频道,即RGB图),输出为64张特征图,卷积核为3x3正方形,为保留原空间分辨率,卷积层的空间填充为1即padding等于1,也就是防止每次卷积尺寸缩小过快导致无法使用更多的卷积层
self.conv1_2 = nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, padding = 1)

self.conv2_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, padding = 1)
self.conv2_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, padding = 1)

self.conv3_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_3 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_4 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)

self.conv4_1 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_3 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_4 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)

self.conv5_1 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_3 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_4 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)

self.relu = nn.ReLU(inplace = True) #inplace=TRUE表示原地操作
self.max = nn.MaxPool2d(kernel_size = 2, stride = 2)

self.fc1 = nn.Linear(512 * 7 * 7, 4096) #定义全连接函数1为线性函数:y = Wx + b,并将512*7*7个节点连接到4096个节点上
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, num_classes)
#定义全连接函数3为线性函数:y = Wx + b,并将4096个节点连接到num_classes个节点上,然后可用softmax进行处理

#定义该神经网络的向前传播函数,该函数必须定义,一旦定义成功,向后传播函数也会自动生成(autograd)
def forward(self, x):

x = self.relu(self.conv1_1(x))
x = self.relu(self.conv1_2(x))
x = self.max(x)
#输入x经过卷积之后,经过激活函数ReLU,循环两次,最后使用2x2的窗口进行最大池化Max pooling,然后更新到x

x = self.relu(self.conv2_1(x))
x = self.relu(self.conv2_2(x))
x = self.max(x)

x = self.relu(self.conv3_1(x))
x = self.relu(self.conv3_2(x))
x = self.relu(self.conv3_3(x))
x = self.relu(self.conv3_4(x))
x = self.max(x)

x = self.relu(self.conv4_1(x))
x = self.relu(self.conv4_2(x))
x = self.relu(self.conv4_3(x))
x = self.relu(self.conv4_4(x))
x = self.max(x)

x = self.relu(self.conv5_1(x))
x = self.relu(self.conv5_2(x))
x = self.relu(self.conv5_3(x))
x = self.relu(self.conv5_4(x))
x = self.max(x)

x = x.view(-1, self.num_flat_features(x)) #view函数将张量x变形成一维的向量形式,总特征数并不改变,为接下来的全连接作准备

x = self.fc1(x) #输入x经过全连接1,然后更新x
x = self.fc2(x)
x = self.fc3(x)
return x

def num_flat_features(self, x):
size = x.size()[1:] #all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features

vgg = VGG()
print(vgg)

我们print网络,可以看到输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VGG(
(conv1_1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv1_2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2_1): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2_2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_1): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_4): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_1): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_4): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_4): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(relu): ReLU(inplace=True)
(max): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(fc1): Linear(in_features=25088, out_features=4096, bias=True)
(fc2): Linear(in_features=4096, out_features=4096, bias=True)
(fc3): Linear(in_features=4096, out_features=1000, bias=True)
)

最后我们随机生成一个张量来进行验证。

1
2
3
input = torch.randn(1, 3, 224, 224)
out = vgg(input)
print(out)

其中(1, 3, 224, 224)表示1个3x224x224的矩阵,这是因为VGG输入的是固定尺寸的224x224的RGB(三通道)图像。
如果没有报错,那么就说明你的神经网络成功运行通过了。
我们也可以使用torch.nn.functional来实现激活函数与池化层,这样的话,你需要还需要多引入一个包:

1
2
3
import torch
import torch.nn as nn
import torch.nn.functional as F #新增

同时,你不需要在init中实例化激活函数与最大池化层,相应的,你需要对forward前馈函数进行更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def forward(self, x):

x = F.relu(self.conv1_1(x))
x = F.relu(self.conv1_2(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)
#输入x经过卷积之后,经过激活函数ReLU,循环两次,最后使用2x2的窗口进行最大池化Max pooling,然后更新到x

x = F.relu(self.conv2_1(x))
x = F.relu(self.conv2_2(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv3_1(x))
x = F.relu(self.conv3_2(x))
x = F.relu(self.conv3_3(x))
x = F.relu(self.conv3_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv4_1(x))
x = F.relu(self.conv4_2(x))
x = F.relu(self.conv4_3(x))
x = F.relu(self.conv4_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv5_1(x))
x = F.relu(self.conv5_2(x))
x = F.relu(self.conv5_3(x))
x = F.relu(self.conv5_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = x.view(-1, self.num_flat_features(x)) #view函数将张量x变形成一维的向量形式,总特征数并不改变,为接下来的全连接作准备

x = self.fc1(x) #输入x经过全连接1,然后更新x
x = self.fc2(x)
x = self.fc3(x)
return x

如果你之前运行通过的话,那么这里也是没有问题的。
这里我想说明一下torch.nntorch.nn.functional的区别。
这两个包中有许多类似的激活函数与损失函数,但是它们又有如下不同:
首先,在定义函数层(继承nn.Module)时,init函数中应该用torch.nn,例如torch.nn.ReLUtorch.nn.Dropout2d,而forward中应该用torch.nn.functional,例如torch.nn.functional.relu,不过请注意,init里面定义的是标准的网络层。只有torch.nn定义的才会进行训练。torch.nn.functional定义的需要自己手动设置参数。所以通常,激活函数或者卷积之类的都用torch.nn定义。
另外,torch.nn是类,必须要先在init中实例化,然后在forward中使用,而torch.nn.functional可以直接在forward中使用。
大家还可以通过官方文档torch.nn.functionaltorch.nn来进一步了解两者的区别。
大家或许发现,我的代码中有大量的重复性工作。是的,你将在文章后面的官方实现中看到优化的代码,但是相对来说,我的代码更加直观些,完全是按照网络的结构顺序从上到下编写的,可以方便初学者(including myself)的理解。


出现的问题

虽然我的代码比较简单直白,但是过程中并不是一帆风顺的,出现了两次报错:

  1. 输入输出不匹配

    当我第一遍运行时,出现了一个RuntimeError: 这是一个超级低级的错误,经学长提醒后我才发现,我两个卷积层之间输出输入的channel数并不匹配: 唉又是ctrl+C+V惹的祸,改正后的网络可以参见上文。
    在这里,我想提醒我自己和大家注意一下卷积层输入输出的维度公式:
    假设输入的宽、高记为W、H。
    超参数中,卷积核的维度是F,stride步长是S,padding是P。
    那么输出的宽X与高Y可用如下公式表示:然而,当我在计算ResNet的维度的时候,发现套用这个公式是除不尽的。于是我搜索到了如下规则:
    1. 对卷积层操作,除不尽时,向下取整。
    2. 对池化层操作,除不尽时,向上取整。
  2. 没有把张量转化成一维向量

    上面的问题解决了,结果还有错误:

    根据报错,可以发现3584x7是等于25088的,结合pytorch官方文档,我意识到我在把张量输入全连接层时,没有把它拍扁成一维。因此,我按照官方文档添加了如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    x = x.view(-1, self.num_flat_features(x))

    def num_flat_features(self, x):
    size = x.size()[1:] #all dimensions except the batch dimension
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    再运行,问题解决。
    注意这里的“except the batch dimension”,我在后面deep-learning笔记:记首次ResNet实战中就踩了坑。

另外,我原本写的代码中,在卷积层之间的对应位置都加上了relu激活函数与池化层。后来我才意识到,由于它们不具有任何需要学习的参数,我可以直接把它们拿出来单独定义:

1
2
self.relu = nn.ReLU(inplace = True)
self.max = nn.MaxPool2d(kernel_size = 2, stride = 2)

虽然是一些很低级的坑,但我还是想写下来供我自己和大家今后参考。


官方源码

由于VGG的结构设计非常有规律,因此官方源码给出了更简洁的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch.nn as nn
import math

class VGG(nn.Module):

def __init__(self, features, num_classes=1000, init_weights=True):
super(VGG, self).__init__()
self.features = features
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
if init_weights:
self._initialize_weights()

def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x

def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
m.weight.data.normal_(0, 0.01)
m.bias.data.zero_()

因为VGG中卷积层的重复性比较高,所以官方使用一个函数来循环产生卷积层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def make_layers(cfg, batch_norm=False):
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)

接下来定义各个版本的卷积层(可参考上文中对论文的截图),这里的“M”表示的是最大池化层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
cfg = {
'A': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'B': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'E': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

def vgg11(**kwargs):
model = VGG(make_layers(cfg['A']), **kwargs)
return model

def vgg11_bn(**kwargs):
model = VGG(make_layers(cfg['A'], batch_norm=True), **kwargs)
return model

def vgg13(**kwargs):
model = VGG(make_layers(cfg['B']), **kwargs)
return model

def vgg13_bn(**kwargs):
model = VGG(make_layers(cfg['B'], batch_norm=True), **kwargs)
return model

def vgg16(**kwargs):
model = VGG(make_layers(cfg['D']), **kwargs)
return model

def vgg16_bn(**kwargs):
model = VGG(make_layers(cfg['D'], batch_norm=True), **kwargs)
return model

def vgg19(**kwargs):
model = VGG(make_layers(cfg['E']), **kwargs)
return model

def vgg19_bn(**kwargs):
model = VGG(make_layers(cfg['E'], batch_norm=True), **kwargs)
return model

if __name__ == '__main__':
# 'VGG', 'vgg11', 'vgg11_bn', 'vgg13', 'vgg13_bn', 'vgg16', 'vgg16_bn', 'vgg19_bn', 'vgg19'
# Example
net11 = vgg11()
print(net11)

附上pytorch官方源码链接。可以在vision/torchvision/models/下找到一系列用pytorch实现的经典神经网络模型。
好了,以上就是VGG的介绍与实现,如有不足之处欢迎大家补充!

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>VGG是我第一个自己编程实践的卷积神经网络,也是挺高兴的,下面我就对VGG在这篇文章中做一个分享。</p><p><strong>Referenc + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>VGG是我第一个自己编程实践的卷积神经网络,也是挺高兴的,下面我就对VGG在这篇文章中做一个分享。</p><p><strong>Referenc @@ -2210,14 +2210,14 @@ - - - - + + + + @@ -2231,13 +2231,13 @@ 2019-09-14T06:25:53.000Z 2020-02-15T23:14:16.543Z -

这是一篇非常经典的有关深度学习的论文,最近在看一个网课的时候又被提到了,因此特地找了pdf文档放在这里和大家分享。之后还分享了一篇deep-learning笔记:近年来深度学习的重要研究成果,个人觉得也值得一读。


简述

这篇文章首先介绍了深度学习的基本前期储备知识、发展背景,并对机器学习范畴内一个重要方向——监督学习进行完整介绍,然后介绍了反向传播算法和微积分链式法则等深度学习基础内容。
文章的接下来重点介绍了卷积神经网络CNN的实现过程、几个非常重要的经典卷积神经网络以及深度卷积神经网络对于视觉任务理解的应用。
文章最后探讨了分布表示和语言模型,循环神经网络RNN原理以及对未来的展望和现实的实现。
总而言之,我觉得这是一篇值得逐字逐句反复阅读咀嚼的文章,读完这篇文章,大概就相当于打开了深度学习的大门了吧。
这篇文章的个人理解与感悟或许我以后会补上,在接触还不深的情况下我不说废话啦,先附上原文,其中黄色高亮部分是一些比较重要的内容,大家有时间的话可以认真看一下。
下面附上原文链接
最后贴一张我觉得挺搞笑的图。

这张图片还有张兄弟图,可以看看我的另一篇论文分享artificial-intelligence笔记:人工智能前沿发展情况分享

]]>
+

这是一篇非常经典的有关深度学习的论文,最近在看一个网课的时候又被提到了,因此特地找了pdf文档放在这里和大家分享。之后还分享了一篇deep-learning笔记:近年来深度学习的重要研究成果,个人觉得也值得一读。


简述

这篇文章首先介绍了深度学习的基本前期储备知识、发展背景,并对机器学习范畴内一个重要方向——监督学习进行完整介绍,然后介绍了反向传播算法和微积分链式法则等深度学习基础内容。
文章的接下来重点介绍了卷积神经网络CNN的实现过程、几个非常重要的经典卷积神经网络以及深度卷积神经网络对于视觉任务理解的应用。
文章最后探讨了分布表示和语言模型,循环神经网络RNN原理以及对未来的展望和现实的实现。
总而言之,我觉得这是一篇值得逐字逐句反复阅读咀嚼的文章,读完这篇文章,大概就相当于打开了深度学习的大门了吧。
这篇文章的个人理解与感悟或许我以后会补上,在接触还不深的情况下我不说废话啦,先附上原文,其中黄色高亮部分是一些比较重要的内容,大家有时间的话可以认真看一下。
下面附上原文链接
最后贴一张我觉得挺搞笑的图。

这张图片还有张兄弟图,可以看看我的另一篇论文分享artificial-intelligence笔记:人工智能前沿发展情况分享

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>这是一篇非常经典的有关深度学习的论文,最近在看一个网课的时候又被提到了,因此特地找了pdf文档放在这里和大家分享。之后还分享了一篇<a href + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>这是一篇非常经典的有关深度学习的论文,最近在看一个网课的时候又被提到了,因此特地找了pdf文档放在这里和大家分享。之后还分享了一篇<a href @@ -2259,13 +2259,13 @@ 2019-09-14T02:00:50.000Z 2020-02-07T08:58:09.671Z -

这是不久前我踩过的一个巨坑,在这里我想先强调一下:
不要升级linux发行版!!!重要的事情说三遍!!!
不要升级linux发行版!!!重要的事情说三遍!!!
不要升级linux发行版!!!重要的事情说三遍!!!
为什么?不要问我为什么。我按系统提示升级ubuntu到18.04LTS后,就再也进不去系统了。不信你可以尝试一下,你将会看到如下界面:

注:图片来自网络,我就不再为了截图而踩一次坑了。

不仅是图形界面,命令行界面也进不去了(据说可以在重启时选择recovery mode并且狂按回车强行进入界面,但我失败了)。
不过如果你真的尝试了并且掉坑里了的话,没关系,你获得了一个很好的重装系统的锻炼机会。下面我们就按步骤锻炼一下。
截止本文最后一次更新前,我已用此方法安装过3遍系统(2遍16.04LTS,1遍18.04LTS),可以放心食用。由于我目前使用的是18.04LTS版本,而ubuntu18.04的社区也日渐活跃,因此本文将会涉及一些18.04版的安装步骤,基本上是一致的。

注:LTS表示的是长期支持版本,一般.04都是LTS的,每两年发布一次,支持期好像是5年。

本文同时适用于ubuntu16.04LTS和ubuntu18.04LTS。

References

电子文献:
https://blog.csdn.net/Spacegene/article/details/86659349


准备

  1. U盘

    准备一个2G以上的无用的U盘,或者备份好里面的文件。然后将其格式化。
  2. 镜像

    下载ubuntu16.04LTS镜像或者ubuntu18.04LTS镜像到本地。也可使用清华源镜像或者阿里云镜像更快下载。
  3. 删除分区

    你可以通过控制面板中的创建并格式化硬盘分区来看到你的windows与ubuntu分区的情况。(以下操作都是针对重装,不再重新分区,需要的可以自行上网查找教程)
    在重新安装ubuntu16.04之前我们需要删除原先Ubuntu的EFI分区及启动引导项,这里推荐直接使用windows下的diskpart来删除。
    使用win+R输入diskpart打开diskpart.exe,允许其对设备进行更改。

    接下来使用list dick,我的笔记本当时只有一块SSD,两个系统都装在上面,故select disk 0进入disk 0。然后就可以输入list partition来查看具体的分区信息。

    其中类型未知的便是分给ubuntu的分区,我这里有一块8G的swap分区和60G的/分区。
    接下来执行如下命令:

    1
    2
    3
    4
    select partition 7
    delete partition override #删除该分区
    select partition 8
    delete partition override #删除该分区

    注意:以上命令是针对我的情况,具体请按照对应ubuntu分区的序号删除。

    现在你可以在控制面板中的创建并格式化硬盘分区中看到你删除的分区已经合并成一块未分配的空间,这也意味着你与你原来ubuntu上的数据彻底说再见了。

  4. 删除ubuntu启动引导项

    首先下载EasyUEFI,使用免费试用版EasyUEFI Trial即可。如果试用期过了的话可以到网上找破解版来下。
    下载完成后安装,打开EasyUEFI如图: 选择管理EFI启动选项Manage EFI Boot Option,然后选择ubuntu启动引导项,点击中间的删除按钮来删除该引导项。 现在重新启动,你会发现已经没有让你选择系统的引导界面,而是直接进入windows系统。
  5. 制作启动U盘

    首先我们下载一个免费的U盘制作工具rufus
    此时插入已经格式化的U盘,打开rufus,一般情况下它会自动选择U盘,你也可以在device选项下手动选择或确认。
    点击select,选择之前下载好的镜像文件。
    其他设置保留默认即可,不放心的话可以比对下图: 然后start开始制作。如果此时rufus提示需要下载一些其它文件,选择Yes继续即可。
    没有问题的话制作完的U盘会如图所示: 在下面的步骤中,请一直插着U盘不要拔。

安装

现在重新启动电脑,开机的过程中不停地快按F12进入bios界面(我的是戴尔的电脑,不同电脑按键或有不同,自行百度;如果快按不行的话再次重启试一试长按,因为网上有些教程说的是长按,而我是长按不行而快按可以)。
随后选择U盘启动(不同电脑这个界面也可能不一样,具体可以百度,我选择的是UEFI BOOT中UEFI:U盘名那项)。
接下来就进入了紫红色的GNU GRUB界面,选择install ubuntu。如果在这里迟疑了一下,会自动进入trying without install,这也没关系,你也可以进入后在图形界面中双击安装,安装之后可以继续试用,直到重启。
随后就是些比较简单的安装过程,基本上可以按默认进行,因为是重装,好像不需要联网安装且有汉化包。

注意:若是安装18.04LTS,这里会出现一个“正常安装”还是“最小安装”的选择,一般无脑选择正常安装即可,但请事先对安装时间做好心里准备(我大概花了一个多小时)且安装完后有些软件还是比较多余的,可以手动卸载。

接下来是比较重要的部分:
进入安装类型installation type界面后,选择其他选项something else,这样我们就可以自己配置ubuntu分区了,点击继续。
接下来会进入一个磁盘分区的界面,选中之前清出来的未分配分区(名为“空闲”,也可以通过大小来判断),点击下方+号,新建一个swap交换分区,大小为8G左右(一般和电脑的内存相当即可,不分这个区会有警告,也可不分之后再加)。
再次双击空闲分区,挂载点下拉,选择/(相当于windows的C盘)。
在安装启动引导器的设备选项中,选择Windows Boot Manager。
结果可以参考下图:

确认无误后点击现在安装,然后就一路默认直到安装完成。
这里会有一个设置用户名和计算机名的界面,建议设置得短一些比较好,否则在终端中每条键入的命令前都会有很长的一串“用户名@计算机名”。
安装完成后可以进行试用,此时一切操作都不会被保留。
如果无需试用,就重新启动系统,此时会提醒可以拔出installation medium即启动U盘。


后期

别忘了把U盘格式化回来,可以继续使用,留着做纪念也行,说不定哪天又要重装。
下面我展示一下我目前的一部分美化效果,亲测发现这只会牺牲一点点儿CPU,所以并不用担心,大胆地美化就是,可能这也是使用linux发行版不多的几种乐趣之一吧。





以上就是重装ubuntu的全部内容,欢迎补充!我也会在新问题出现时及时更新。

]]>
+

这是不久前我踩过的一个巨坑,在这里我想先强调一下:
不要升级linux发行版!!!重要的事情说三遍!!!
不要升级linux发行版!!!重要的事情说三遍!!!
不要升级linux发行版!!!重要的事情说三遍!!!
为什么?不要问我为什么。我按系统提示升级ubuntu到18.04LTS后,就再也进不去系统了。不信你可以尝试一下,你将会看到如下界面:

注:图片来自网络,我就不再为了截图而踩一次坑了。

不仅是图形界面,命令行界面也进不去了(据说可以在重启时选择recovery mode并且狂按回车强行进入界面,但我失败了)。
不过如果你真的尝试了并且掉坑里了的话,没关系,你获得了一个很好的重装系统的锻炼机会。下面我们就按步骤锻炼一下。
截止本文最后一次更新前,我已用此方法安装过3遍系统(2遍16.04LTS,1遍18.04LTS),可以放心食用。由于我目前使用的是18.04LTS版本,而ubuntu18.04的社区也日渐活跃,因此本文将会涉及一些18.04版的安装步骤,基本上是一致的。

注:LTS表示的是长期支持版本,一般.04都是LTS的,每两年发布一次,支持期好像是5年。

本文同时适用于ubuntu16.04LTS和ubuntu18.04LTS。

References

电子文献:
https://blog.csdn.net/Spacegene/article/details/86659349


准备

  1. U盘

    准备一个2G以上的无用的U盘,或者备份好里面的文件。然后将其格式化。
  2. 镜像

    下载ubuntu16.04LTS镜像或者ubuntu18.04LTS镜像到本地。也可使用清华源镜像或者阿里云镜像更快下载。
  3. 删除分区

    你可以通过控制面板中的创建并格式化硬盘分区来看到你的windows与ubuntu分区的情况。(以下操作都是针对重装,不再重新分区,需要的可以自行上网查找教程)
    在重新安装ubuntu16.04之前我们需要删除原先Ubuntu的EFI分区及启动引导项,这里推荐直接使用windows下的diskpart来删除。
    使用win+R输入diskpart打开diskpart.exe,允许其对设备进行更改。

    接下来使用list dick,我的笔记本当时只有一块SSD,两个系统都装在上面,故select disk 0进入disk 0。然后就可以输入list partition来查看具体的分区信息。

    其中类型未知的便是分给ubuntu的分区,我这里有一块8G的swap分区和60G的/分区。
    接下来执行如下命令:

    1
    2
    3
    4
    select partition 7
    delete partition override #删除该分区
    select partition 8
    delete partition override #删除该分区

    注意:以上命令是针对我的情况,具体请按照对应ubuntu分区的序号删除。

    现在你可以在控制面板中的创建并格式化硬盘分区中看到你删除的分区已经合并成一块未分配的空间,这也意味着你与你原来ubuntu上的数据彻底说再见了。

  4. 删除ubuntu启动引导项

    首先下载EasyUEFI,使用免费试用版EasyUEFI Trial即可。如果试用期过了的话可以到网上找破解版来下。
    下载完成后安装,打开EasyUEFI如图: 选择管理EFI启动选项Manage EFI Boot Option,然后选择ubuntu启动引导项,点击中间的删除按钮来删除该引导项。 现在重新启动,你会发现已经没有让你选择系统的引导界面,而是直接进入windows系统。
  5. 制作启动U盘

    首先我们下载一个免费的U盘制作工具rufus
    此时插入已经格式化的U盘,打开rufus,一般情况下它会自动选择U盘,你也可以在device选项下手动选择或确认。
    点击select,选择之前下载好的镜像文件。
    其他设置保留默认即可,不放心的话可以比对下图: 然后start开始制作。如果此时rufus提示需要下载一些其它文件,选择Yes继续即可。
    没有问题的话制作完的U盘会如图所示: 在下面的步骤中,请一直插着U盘不要拔。

安装

现在重新启动电脑,开机的过程中不停地快按F12进入bios界面(我的是戴尔的电脑,不同电脑按键或有不同,自行百度;如果快按不行的话再次重启试一试长按,因为网上有些教程说的是长按,而我是长按不行而快按可以)。
随后选择U盘启动(不同电脑这个界面也可能不一样,具体可以百度,我选择的是UEFI BOOT中UEFI:U盘名那项)。
接下来就进入了紫红色的GNU GRUB界面,选择install ubuntu。如果在这里迟疑了一下,会自动进入trying without install,这也没关系,你也可以进入后在图形界面中双击安装,安装之后可以继续试用,直到重启。
随后就是些比较简单的安装过程,基本上可以按默认进行,因为是重装,好像不需要联网安装且有汉化包。

注意:若是安装18.04LTS,这里会出现一个“正常安装”还是“最小安装”的选择,一般无脑选择正常安装即可,但请事先对安装时间做好心里准备(我大概花了一个多小时)且安装完后有些软件还是比较多余的,可以手动卸载。

接下来是比较重要的部分:
进入安装类型installation type界面后,选择其他选项something else,这样我们就可以自己配置ubuntu分区了,点击继续。
接下来会进入一个磁盘分区的界面,选中之前清出来的未分配分区(名为“空闲”,也可以通过大小来判断),点击下方+号,新建一个swap交换分区,大小为8G左右(一般和电脑的内存相当即可,不分这个区会有警告,也可不分之后再加)。
再次双击空闲分区,挂载点下拉,选择/(相当于windows的C盘)。
在安装启动引导器的设备选项中,选择Windows Boot Manager。
结果可以参考下图:

确认无误后点击现在安装,然后就一路默认直到安装完成。
这里会有一个设置用户名和计算机名的界面,建议设置得短一些比较好,否则在终端中每条键入的命令前都会有很长的一串“用户名@计算机名”。
安装完成后可以进行试用,此时一切操作都不会被保留。
如果无需试用,就重新启动系统,此时会提醒可以拔出installation medium即启动U盘。


后期

别忘了把U盘格式化回来,可以继续使用,留着做纪念也行,说不定哪天又要重装。
下面我展示一下我目前的一部分美化效果,亲测发现这只会牺牲一点点儿CPU,所以并不用担心,大胆地美化就是,可能这也是使用linux发行版不多的几种乐趣之一吧。





以上就是重装ubuntu的全部内容,欢迎补充!我也会在新问题出现时及时更新。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>这是不久前我踩过的一个巨坑,在这里我想先强调一下:<br><strong>不要升级linux发行版!!!重要的事情说三遍!!!</strong> + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>这是不久前我踩过的一个巨坑,在这里我想先强调一下:<br><strong>不要升级linux发行版!!!重要的事情说三遍!!!</strong> @@ -2291,13 +2291,13 @@ 2019-09-14T01:48:53.000Z 2020-01-26T02:02:25.357Z -

前一篇讲了如何清理windows下的空间,然而虽然ubuntu中垃圾文件没win10那么多,可是我给ubuntu分配的空间比win10少得多了,于是我又找了些清理ubuntu的方法。

References

电子文献:
https://www.jb51.net/article/164589.htm
https://blog.csdn.net/m0_37407756/article/details/79903837


查看

我们可以在终端中使用df命令来查看磁盘的利用情况。
另外,可以加一个-h即“human reading”使显示的磁盘利用状况列表更加适合我们阅读(主要是转化了单位和列名)。


方法

  1. 清理apt缓存文件

    ubuntu在/var/cache/apt/archives目录中会保留deb软件包的缓冲文件。随着时间的推移,这些缓存可能会占有很多空间。
    我们可以使用sudo du -sh /var/cache/apt来查看当前apt缓存文件的占用的大小。
    我们可以直接在终端执行如下命令以清理过时的软件包:

    1
    sudo apt-get autoclean

    我们可以在终端中执行以下命令来移除所有apt缓存中的软件包:

    1
    sudo apt-get clean

    实践证明,这两条命令其实清理得不是非常干净(会剩下kb级的缓存),不过如果很久没清理的话,还是非常强力的。

  2. 删除其他软件依赖的但现在已不用的软件包

    下面这条命令可以移除系统不再需要的依赖库和软件包。这些软件包是自动安装的,是当初为了使得某个安装的软件包满足依赖关系,而此时已不再需要。

    1
    sudo apt-get autoremove

    除了移除不再被系统需要的孤立软件包,这条命令也会移除安装在系统中的linux旧内核(有更精确的操作方法,有点专业,这里就不说了)。
    注意,这条命令执行后,软件的配置文件还是会保留的。
    可以使用purge选项来同时清除软件包和软件的配置文件。

    1
    sudo apt-get autoremove --purge

    补充:这里扯点题外话,最近看到一个挺好用的命令apt-get install -f,其作用是修复依赖关系(depends),即假如系统上有某个package不满足依赖条件,这个命令就会自动安装那个package所依赖的package。

  3. 清除缩略图缓存

    可以使用du -sh ~/.cache/thumbnails/查看缩略图缓存占用的空间。
    其实如果不是摄影爱好者或者类似的使用者的话,这个缓存不会特别大,不过对缓存强迫症患者还是可以清一下的。

    1
    rm -rf ~/.cache/thumbnails/*
  4. 清除残余配置文件

    可以使用dpkg --list | grep "^rc"查看残余的配置文件,如果没有的话,可以跳过后文。
    这里的rc表示软件包已经删除(Remove),但配置文件(Config-file)还在的文件。
    这里具体的介绍可以看一下我新写的文章ubuntu笔记:安装与卸载deb软件包
    若有,咱们来删除:

    1
    dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P

    或者

    1
    dpkg --list | grep "^rc" | cut -d " " -f 3 | xargs sudo dpkg --purge

    这时候如果出现如下错误,那无需担心,因为已经不存在残余的配置文件了。

可以把上面的命令按顺序执行一遍,就完成了对ubuntu系统的空间释放。

]]>
+

前一篇讲了如何清理windows下的空间,然而虽然ubuntu中垃圾文件没win10那么多,可是我给ubuntu分配的空间比win10少得多了,于是我又找了些清理ubuntu的方法。

References

电子文献:
https://www.jb51.net/article/164589.htm
https://blog.csdn.net/m0_37407756/article/details/79903837


查看

我们可以在终端中使用df命令来查看磁盘的利用情况。
另外,可以加一个-h即“human reading”使显示的磁盘利用状况列表更加适合我们阅读(主要是转化了单位和列名)。


方法

  1. 清理apt缓存文件

    ubuntu在/var/cache/apt/archives目录中会保留deb软件包的缓冲文件。随着时间的推移,这些缓存可能会占有很多空间。
    我们可以使用sudo du -sh /var/cache/apt来查看当前apt缓存文件的占用的大小。
    我们可以直接在终端执行如下命令以清理过时的软件包:

    1
    sudo apt-get autoclean

    我们可以在终端中执行以下命令来移除所有apt缓存中的软件包:

    1
    sudo apt-get clean

    实践证明,这两条命令其实清理得不是非常干净(会剩下kb级的缓存),不过如果很久没清理的话,还是非常强力的。

  2. 删除其他软件依赖的但现在已不用的软件包

    下面这条命令可以移除系统不再需要的依赖库和软件包。这些软件包是自动安装的,是当初为了使得某个安装的软件包满足依赖关系,而此时已不再需要。

    1
    sudo apt-get autoremove

    除了移除不再被系统需要的孤立软件包,这条命令也会移除安装在系统中的linux旧内核(有更精确的操作方法,有点专业,这里就不说了)。
    注意,这条命令执行后,软件的配置文件还是会保留的。
    可以使用purge选项来同时清除软件包和软件的配置文件。

    1
    sudo apt-get autoremove --purge

    补充:这里扯点题外话,最近看到一个挺好用的命令apt-get install -f,其作用是修复依赖关系(depends),即假如系统上有某个package不满足依赖条件,这个命令就会自动安装那个package所依赖的package。

  3. 清除缩略图缓存

    可以使用du -sh ~/.cache/thumbnails/查看缩略图缓存占用的空间。
    其实如果不是摄影爱好者或者类似的使用者的话,这个缓存不会特别大,不过对缓存强迫症患者还是可以清一下的。

    1
    rm -rf ~/.cache/thumbnails/*
  4. 清除残余配置文件

    可以使用dpkg --list | grep "^rc"查看残余的配置文件,如果没有的话,可以跳过后文。
    这里的rc表示软件包已经删除(Remove),但配置文件(Config-file)还在的文件。
    这里具体的介绍可以看一下我新写的文章ubuntu笔记:安装与卸载deb软件包
    若有,咱们来删除:

    1
    dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P

    或者

    1
    dpkg --list | grep "^rc" | cut -d " " -f 3 | xargs sudo dpkg --purge

    这时候如果出现如下错误,那无需担心,因为已经不存在残余的配置文件了。

可以把上面的命令按顺序执行一遍,就完成了对ubuntu系统的空间释放。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>前一篇讲了如何清理windows下的空间,然而虽然ubuntu中垃圾文件没win10那么多,可是我给ubuntu分配的空间比win10少得多了, + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>前一篇讲了如何清理windows下的空间,然而虽然ubuntu中垃圾文件没win10那么多,可是我给ubuntu分配的空间比win10少得多了, @@ -2319,13 +2319,13 @@ 2019-09-14T01:10:23.000Z 2020-02-25T00:29:04.268Z -

暑假里想跑CVPR中的代码,发现作者提供的环境配置都是基于linux终端的,这样windows的git bash就满足不了我了。二话不说我花了两天时间装了个ubuntu+windows双系统,好不容易装好了,却发现我的硬盘空间已经岌岌可危(理论上要留内存的三倍左右可以保证系统顺畅运行,我的内存是8G,也就是说我C盘应空出20G左右为宜)。于是我就找了些释放空间的办法,分享在这里。

References

电子文献:
http://www.udaxia.com/wtjd/9147.html


利用磁盘属性进行清理

这是最稳的方法,但释放的空间也相对较少,不过还是有效的。选择“此电脑”,右键C盘,属性,然后就可以在常规下面看到磁盘清理。一般按默认选择的进行清理,当然全点上勾也无所谓。

注意:千万不能选择压缩此驱动器以节约磁盘空间!

另外在工具下面你可以看到一个优化的选项,一般系统会定期自动执行优化,如果你是强迫症,时时刻刻都容不得一点冗余的话,可以手动优化。

据我们数据结构的老师说,由于数据在存储时大多是稀疏矩阵,存在许多的空间浪费,而磁盘碎片整理优化的就是这个。


清理系统文件

在前面的磁盘清理界面中我们还可以看到清理系统文件这一选项,可以选择它进行进一步清理,这里面有一项是以前的windows安装文件,如果不打算回退的话清理无妨。

有可能你还会注意到一个名为“系统错误内存转储文件”的选项,这个也可以大胆清除,对一般使用者(基本不需排查系统问题)这个文件完全没有任何作用。

对于防止系统错误内存转储文件占用空间,还有一劳永逸直接禁止生成的办法,首先打开高级系统设置,来到如图所示选项卡。

打开“启动和故障修复”设置窗口,在写入调试信息的下拉列表中选择“无”并确定即可。

当然,如果你觉得你或许用得上系统错误内存转储文件,那么也可以选择这里的小内存转储或者核心内存转储,这样同样也能节省空间。
清理系统文件是给windows10瘦身最有效的办法之一,事实上,若无特殊需求,扫描结果中的文件均可以勾选清除。


删除临时文件

这里有两个临时文件中的全部文件可以删除,一个是C:\Users\用户名\AppData\Local\Temp目录下的文件,这里是临时文件最多的地方,可以上到几个G;另一个是C:\Windows\Temp,这里文件大小相对较小,可以忽略不计。另外我也找到了一些其他的临时文件,但似乎它们的体积都是0,可能是系统自动清理了。

注意:千万不要误删上一级目录!如果担心删除出错,可先放到回收站,重启之后看有无异常再做决定。亲测上述两个文件夹中的所有文件均可删除。


删除冗余更新

众所周知,windows系统会自动更新,这也是许多人弃windows的一大原因,然而windows还是要用的,于是我找到了一种可以清理多余的windows更新文件的方法。

  1. 首先右键左下角开始菜单,选择“Windows PowerShell(管理员)”。 弹出是否允许进行更改选择“是”。
  2. 输入命令dism.exe /Online /Cleanup-Image /AnalyzeComponentStore并执行。
  3. 这时候会显示“推荐使用组件存储清理:是or否”,因为我前不久清过,所以这里显示为“否”,那么就别清理了。如果显示为“是”,那么进行第四步。
  4. 输入命令dism.exe /online /Cleanup-Image /StartComponentCleanup并执行。
    这样电脑就会开始清理啦。这个过程会比较长,不要着急和担心,在这期间你可以做些别的事情,比如看看我其他的几篇博客。

关闭休眠功能

电脑开启休眠功能后,系统自动生成内存镜像文件,以便唤醒电脑以后可以快速开启程序。不过休眠有个缺点,那就是会占用大部分的系统盘空间(约3G),这个文件在C盘的根目录,名为hiberfil.sys,若觉得这个功能不必要的话,就可以关闭休眠以节省磁盘空间。

  1. 打开“Windows PowerShell(管理员)”,方法同上。
  2. 输入powercfg -h off,回车。
    这样就成功关闭了休眠功能。此时Cpan内已没有hiberfil.sys这个文件了。

开启自动清理

打开存储设置,开启存储感知,这样系统就会自动清理了。
可以通过“配置存储感知或立即运行”选项来调整自动清理方式或者立即清理,这种方法也很强力,能释放上G的空间,推荐使用!


重装系统

俗话说得好“大力出奇迹”,重装系统无疑是最有效清理空间的一种方法。只要备份好数据,重装其实没有想象的那么困难。我本人这一年多来就重装过3次系统(两次是被迫的…),其实装了几次就熟练了,我曾看到某linux大牛(忘了是谁)总共重装了19次linux。你可以在我的另一篇博文ubuntu笔记:重装ubuntu——记一次辛酸血泪史中看到如何在双系统情况下重装ubuntu的过程。


软件安装管家

本文中的部分方法来源于公众号“软件安装管家”的推送,在这里安利一下。该公众号提供了丰富且稳定的破解软件安装(VS、AD、PS等),此外,这个公众号的客服还是一个难得的段子手!刷评论区绝对可以成为你的快乐源泉哈哈。

]]>
+

暑假里想跑CVPR中的代码,发现作者提供的环境配置都是基于linux终端的,这样windows的git bash就满足不了我了。二话不说我花了两天时间装了个ubuntu+windows双系统,好不容易装好了,却发现我的硬盘空间已经岌岌可危(理论上要留内存的三倍左右可以保证系统顺畅运行,我的内存是8G,也就是说我C盘应空出20G左右为宜)。于是我就找了些释放空间的办法,分享在这里。

References

电子文献:
http://www.udaxia.com/wtjd/9147.html


利用磁盘属性进行清理

这是最稳的方法,但释放的空间也相对较少,不过还是有效的。选择“此电脑”,右键C盘,属性,然后就可以在常规下面看到磁盘清理。一般按默认选择的进行清理,当然全点上勾也无所谓。

注意:千万不能选择压缩此驱动器以节约磁盘空间!

另外在工具下面你可以看到一个优化的选项,一般系统会定期自动执行优化,如果你是强迫症,时时刻刻都容不得一点冗余的话,可以手动优化。

据我们数据结构的老师说,由于数据在存储时大多是稀疏矩阵,存在许多的空间浪费,而磁盘碎片整理优化的就是这个。


清理系统文件

在前面的磁盘清理界面中我们还可以看到清理系统文件这一选项,可以选择它进行进一步清理,这里面有一项是以前的windows安装文件,如果不打算回退的话清理无妨。

有可能你还会注意到一个名为“系统错误内存转储文件”的选项,这个也可以大胆清除,对一般使用者(基本不需排查系统问题)这个文件完全没有任何作用。

对于防止系统错误内存转储文件占用空间,还有一劳永逸直接禁止生成的办法,首先打开高级系统设置,来到如图所示选项卡。

打开“启动和故障修复”设置窗口,在写入调试信息的下拉列表中选择“无”并确定即可。

当然,如果你觉得你或许用得上系统错误内存转储文件,那么也可以选择这里的小内存转储或者核心内存转储,这样同样也能节省空间。
清理系统文件是给windows10瘦身最有效的办法之一,事实上,若无特殊需求,扫描结果中的文件均可以勾选清除。


删除临时文件

这里有两个临时文件中的全部文件可以删除,一个是C:\Users\用户名\AppData\Local\Temp目录下的文件,这里是临时文件最多的地方,可以上到几个G;另一个是C:\Windows\Temp,这里文件大小相对较小,可以忽略不计。另外我也找到了一些其他的临时文件,但似乎它们的体积都是0,可能是系统自动清理了。

注意:千万不要误删上一级目录!如果担心删除出错,可先放到回收站,重启之后看有无异常再做决定。亲测上述两个文件夹中的所有文件均可删除。


删除冗余更新

众所周知,windows系统会自动更新,这也是许多人弃windows的一大原因,然而windows还是要用的,于是我找到了一种可以清理多余的windows更新文件的方法。

  1. 首先右键左下角开始菜单,选择“Windows PowerShell(管理员)”。 弹出是否允许进行更改选择“是”。
  2. 输入命令dism.exe /Online /Cleanup-Image /AnalyzeComponentStore并执行。
  3. 这时候会显示“推荐使用组件存储清理:是or否”,因为我前不久清过,所以这里显示为“否”,那么就别清理了。如果显示为“是”,那么进行第四步。
  4. 输入命令dism.exe /online /Cleanup-Image /StartComponentCleanup并执行。
    这样电脑就会开始清理啦。这个过程会比较长,不要着急和担心,在这期间你可以做些别的事情,比如看看我其他的几篇博客。

关闭休眠功能

电脑开启休眠功能后,系统自动生成内存镜像文件,以便唤醒电脑以后可以快速开启程序。不过休眠有个缺点,那就是会占用大部分的系统盘空间(约3G),这个文件在C盘的根目录,名为hiberfil.sys,若觉得这个功能不必要的话,就可以关闭休眠以节省磁盘空间。

  1. 打开“Windows PowerShell(管理员)”,方法同上。
  2. 输入powercfg -h off,回车。
    这样就成功关闭了休眠功能。此时Cpan内已没有hiberfil.sys这个文件了。

开启自动清理

打开存储设置,开启存储感知,这样系统就会自动清理了。
可以通过“配置存储感知或立即运行”选项来调整自动清理方式或者立即清理,这种方法也很强力,能释放上G的空间,推荐使用!


重装系统

俗话说得好“大力出奇迹”,重装系统无疑是最有效清理空间的一种方法。只要备份好数据,重装其实没有想象的那么困难。我本人这一年多来就重装过3次系统(两次是被迫的…),其实装了几次就熟练了,我曾看到某linux大牛(忘了是谁)总共重装了19次linux。你可以在我的另一篇博文ubuntu笔记:重装ubuntu——记一次辛酸血泪史中看到如何在双系统情况下重装ubuntu的过程。


软件安装管家

本文中的部分方法来源于公众号“软件安装管家”的推送,在这里安利一下。该公众号提供了丰富且稳定的破解软件安装(VS、AD、PS等),此外,这个公众号的客服还是一个难得的段子手!刷评论区绝对可以成为你的快乐源泉哈哈。

]]>
- <!-- build time:Sat May 02 2020 23:29:05 GMT+0800 (GMT+08:00) --><p>暑假里想跑CVPR中的代码,发现作者提供的环境配置都是基于linux终端的,这样windows的git bash就满足不了我了。二话不说我花了两 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>暑假里想跑CVPR中的代码,发现作者提供的环境配置都是基于linux终端的,这样windows的git bash就满足不了我了。二话不说我花了两 @@ -2347,13 +2347,13 @@ 2019-09-14T00:02:24.000Z 2020-02-07T08:55:58.528Z -

大一有一段时间,我沉迷于web前端制作网页,比较熟练地掌握了html的语法,还根据需要接触了一些CSS以及js的内容。说白了,html只是一种标记语言(不属于编程语言),但是它简单易学,且很容易获得可视化的效果,对于培养兴趣而言我感觉是很有帮助的。油管up主,现哈佛在读学霸John Fish(请科学上网)当初就是从html进入计算机世界的。下面贴一个我自己做的网页,是综合web三大语言编写的,大一的时候把自己需要的网站都放上面了,也有一种归属感吧。



主页上那个是python之禅,也是我很喜欢的一段文字,在python环境下import this就可以看到。由于上面的网页是我学web的时候边看书边编的,各种元素都尝试了一下,最后也没有美化一直到现在,所以大佬们勿喷哈。


小技巧

我这里强烈推荐使用VScode写前端,它有很多强大的插件,我这里推荐其中一个吧。

如介绍所写,使用alt+B快捷键可以直接在默认浏览器下查看你写的网页,而shift+alt+B可以选浏览器查看,因为有些时候microsoft自带的edge浏览器无法实现你编写的效果(巨坑),推荐使用chrome打开浏览。使用这个插件能让你更快捷地预览你编写的效果并进行修改,大大提高了效率。
其他的插件网上有很多推荐,也等待着你自己去发现,这里就不一一列出了。
还有一个快捷的操作就是快速生成代码块,在VScode中是这样操作的(其他编辑器也应该类似):

  1. 输入一个
  2. 按tab键或者回车: 这样就可以节省很多时间,非常方便。

小问题

在我想使用web来打开我本地的txt文件时,我遇到过这样一个问题:打开的中文文档在浏览器中显示为乱码。在尝试其他浏览器后,我发现这不是浏览器的问题。最后我大致找到了两种解决办法:

  1. 一种是找到head下面的meta charset,修改代码如下:
  2. 另一种是另存为文件时修改一下格式,这里我们修改成“UTF-8”。另外我室友在使用python导入文件的时候也因为格式导致报错,修改成“ANSI”后即可。 希望通过上面两种方法的尝试能让你解决乱码问题,几种编码格式的区别在这里暂不说明,以后有空补上。
]]>
+

大一有一段时间,我沉迷于web前端制作网页,比较熟练地掌握了html的语法,还根据需要接触了一些CSS以及js的内容。说白了,html只是一种标记语言(不属于编程语言),但是它简单易学,且很容易获得可视化的效果,对于培养兴趣而言我感觉是很有帮助的。油管up主,现哈佛在读学霸John Fish(请科学上网)当初就是从html进入计算机世界的。下面贴一个我自己做的网页,是综合web三大语言编写的,大一的时候把自己需要的网站都放上面了,也有一种归属感吧。



主页上那个是python之禅,也是我很喜欢的一段文字,在python环境下import this就可以看到。由于上面的网页是我学web的时候边看书边编的,各种元素都尝试了一下,最后也没有美化一直到现在,所以大佬们勿喷哈。


小技巧

我这里强烈推荐使用VScode写前端,它有很多强大的插件,我这里推荐其中一个吧。

如介绍所写,使用alt+B快捷键可以直接在默认浏览器下查看你写的网页,而shift+alt+B可以选浏览器查看,因为有些时候microsoft自带的edge浏览器无法实现你编写的效果(巨坑),推荐使用chrome打开浏览。使用这个插件能让你更快捷地预览你编写的效果并进行修改,大大提高了效率。
其他的插件网上有很多推荐,也等待着你自己去发现,这里就不一一列出了。
还有一个快捷的操作就是快速生成代码块,在VScode中是这样操作的(其他编辑器也应该类似):

  1. 输入一个
  2. 按tab键或者回车: 这样就可以节省很多时间,非常方便。

小问题

在我想使用web来打开我本地的txt文件时,我遇到过这样一个问题:打开的中文文档在浏览器中显示为乱码。在尝试其他浏览器后,我发现这不是浏览器的问题。最后我大致找到了两种解决办法:

  1. 一种是找到head下面的meta charset,修改代码如下:
  2. 另一种是另存为文件时修改一下格式,这里我们修改成“UTF-8”。另外我室友在使用python导入文件的时候也因为格式导致报错,修改成“ANSI”后即可。 希望通过上面两种方法的尝试能让你解决乱码问题,几种编码格式的区别在这里暂不说明,以后有空补上。
]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>大一有一段时间,我沉迷于web前端制作网页,比较熟练地掌握了html的语法,还根据需要接触了一些CSS以及js的内容。说白了,html只是一种标 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>大一有一段时间,我沉迷于web前端制作网页,比较熟练地掌握了html的语法,还根据需要接触了一些CSS以及js的内容。说白了,html只是一种标 @@ -2373,13 +2373,13 @@ 2019-09-13T15:17:48.000Z 2020-02-19T09:28:18.823Z -

anaconda是一个开源的包、环境管理器,可以比较有效地配置多个虚拟环境,当python入门到一定程度时,安装anaconda是很必要的。前段时间室友学习python的时候问到过我一些相关的问题,我就在这里简单写一些我知道的以及我搜集到的知识。


环境变量

安装anaconda过程中一个很重要的步骤就是配置环境变量,网上有很多手动添加环境变量的教程,其实很简单,只需添加三个路径,当然更简单的是直接在安装的时候添加到path(可以无视warning)。我想在这里写的是环境变量的概念问题,其实直到不久前帮同学安装我才明白。
环境变量是指在操作系统中用来指定操作系统运行环境的一些参数。当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还会到path中指定的路径去找。这就是为什么不添加C:\Users\用户名\Anaconda3\Scripts到path就无法执行conda命令,因为此时conda.exe无法被找到。


conda与pip

利用conda installpip install命令来安装各种包的过程中,想必你也对两者之间的区别很疑惑,下面我就总结一下我搜集到的相关解答。
简而言之,pip是python包的通用管理器,而conda是一个与语言无关的跨平台环境管理器。对我们而言,最显着的区别可能是这样的:pip在任何环境中安装python包,conda安装在conda环境中装任何包。因此往往conda list的数量会大于pip list。
要注意的是,如果使用conda安装多个环境时,对于同一个包只需要安装一次,有conda集中进行管理。
但是如果使用pip,因为每个环境安装使用的pip在不同的路径下,故会重复安装,而包会从缓存中取。
总的来说,我推荐尽早安装anaconda并且使用conda来管理python的各种包。


升级

我们可以在命令行中或者anaconda prompt中执行命令进行操作。

1
2
3
conda update conda #升级conda
conda update anaconda #升级anaconda前要先升级conda
conda update --all #升级所有包

在升级完成之后,我们可以使用命令来清理一些无用的包以释放一些空间:

1
2
conda clean -p #删除没有用的包
conda clean -t #删除保存下来的压缩文件(.tar)


虚拟环境

conda list命令用于查看conda下的包,而conda env list命令可以用来查看conda创建的所有虚拟环境。

下面就简述一下如何创建这些虚拟环境。
使用如下命令,可以创建一个新的环境:
conda create -n Python27 python=2.7
其中Python27是自定义的一个名称,而python=2.7是一个格式,可以变动等号右边的数字来改变python环境的kernel版本,这里我们安装的是python2.7版本(将于2020年停止维护)。
在anaconda prompt中,我们可以看到我们处在的是base环境下,也就是我安装的python3环境下,我们可以使用下面两个命令来切换环境:

注意:如果直接在ubuntu的命令行中切换环境,需要加上conda,比如conda activate 环境名

在创建环境的过程中,难免会不小心取了个难听的环境名,别担心,我们有方法来删除环境。
conda remove -n 难听的名字 --all
有时候一个环境已经配置好了,但我们想要重命名,这怎么办呢?可以这样办:

1
2
conda create -n 新名字 --clone 老名字
conda remove -n 老名字 --all

把环境添加到jupyter notebook

首先通过activate进入想要添加的环境中,然后安装ipykernel,接下来就可以进行添加了。

1
2
pip install ipykernel
python -m ipykernel install --name Python27 #Python27可以取与环境名不一样的名字,但方便起见建议统一

我们可以使用jupyter kernelspec list来查看已添加到jupyter notebook的kernel。
显示如下:

我们也可以在jupyter notebook中的new或者kernel下查看新环境是否成功添加。
若想删除某个指定的kernel,可以使用命令jupyter kernelspec remove kernel_name来完成。
在这里我想说明一下为什么要分开python的环境。
由于python是不向后兼容的,分开环境可以避免语法版本不一引起的错误,同时这也可以避免工具包安装与调用的混乱。

]]>
+

anaconda是一个开源的包、环境管理器,可以比较有效地配置多个虚拟环境,当python入门到一定程度时,安装anaconda是很必要的。前段时间室友学习python的时候问到过我一些相关的问题,我就在这里简单写一些我知道的以及我搜集到的知识。


环境变量

安装anaconda过程中一个很重要的步骤就是配置环境变量,网上有很多手动添加环境变量的教程,其实很简单,只需添加三个路径,当然更简单的是直接在安装的时候添加到path(可以无视warning)。我想在这里写的是环境变量的概念问题,其实直到不久前帮同学安装我才明白。
环境变量是指在操作系统中用来指定操作系统运行环境的一些参数。当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还会到path中指定的路径去找。这就是为什么不添加C:\Users\用户名\Anaconda3\Scripts到path就无法执行conda命令,因为此时conda.exe无法被找到。


conda与pip

利用conda installpip install命令来安装各种包的过程中,想必你也对两者之间的区别很疑惑,下面我就总结一下我搜集到的相关解答。
简而言之,pip是python包的通用管理器,而conda是一个与语言无关的跨平台环境管理器。对我们而言,最显着的区别可能是这样的:pip在任何环境中安装python包,conda安装在conda环境中装任何包。因此往往conda list的数量会大于pip list。
要注意的是,如果使用conda安装多个环境时,对于同一个包只需要安装一次,有conda集中进行管理。
但是如果使用pip,因为每个环境安装使用的pip在不同的路径下,故会重复安装,而包会从缓存中取。
总的来说,我推荐尽早安装anaconda并且使用conda来管理python的各种包。


升级

我们可以在命令行中或者anaconda prompt中执行命令进行操作。

1
2
3
conda update conda #升级conda
conda update anaconda #升级anaconda前要先升级conda
conda update --all #升级所有包

在升级完成之后,我们可以使用命令来清理一些无用的包以释放一些空间:

1
2
conda clean -p #删除没有用的包
conda clean -t #删除保存下来的压缩文件(.tar)


虚拟环境

conda list命令用于查看conda下的包,而conda env list命令可以用来查看conda创建的所有虚拟环境。

下面就简述一下如何创建这些虚拟环境。
使用如下命令,可以创建一个新的环境:
conda create -n Python27 python=2.7
其中Python27是自定义的一个名称,而python=2.7是一个格式,可以变动等号右边的数字来改变python环境的kernel版本,这里我们安装的是python2.7版本(将于2020年停止维护)。
在anaconda prompt中,我们可以看到我们处在的是base环境下,也就是我安装的python3环境下,我们可以使用下面两个命令来切换环境:

注意:如果直接在ubuntu的命令行中切换环境,需要加上conda,比如conda activate 环境名

在创建环境的过程中,难免会不小心取了个难听的环境名,别担心,我们有方法来删除环境。
conda remove -n 难听的名字 --all
有时候一个环境已经配置好了,但我们想要重命名,这怎么办呢?可以这样办:

1
2
conda create -n 新名字 --clone 老名字
conda remove -n 老名字 --all

把环境添加到jupyter notebook

首先通过activate进入想要添加的环境中,然后安装ipykernel,接下来就可以进行添加了。

1
2
pip install ipykernel
python -m ipykernel install --name Python27 #Python27可以取与环境名不一样的名字,但方便起见建议统一

我们可以使用jupyter kernelspec list来查看已添加到jupyter notebook的kernel。
显示如下:

我们也可以在jupyter notebook中的new或者kernel下查看新环境是否成功添加。
若想删除某个指定的kernel,可以使用命令jupyter kernelspec remove kernel_name来完成。
在这里我想说明一下为什么要分开python的环境。
由于python是不向后兼容的,分开环境可以避免语法版本不一引起的错误,同时这也可以避免工具包安装与调用的混乱。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>anaconda是一个开源的包、环境管理器,可以比较有效地配置多个虚拟环境,当python入门到一定程度时,安装anaconda是很必要的。前段 + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>anaconda是一个开源的包、环境管理器,可以比较有效地配置多个虚拟环境,当python入门到一定程度时,安装anaconda是很必要的。前段 @@ -2399,13 +2399,13 @@ 2019-09-13T14:50:22.000Z 2019-11-10T09:11:43.074Z -

首先强烈安利jupyter notebook,它是一种交互式笔记本,安装anaconda的时候会一并安装,下载VS的时候也可以选择安装。
我是在初学机器学习的时候接触jupyter notebook的,立刻就被它便捷的交互与结果呈现方式所吸引,现在python编程基本不使用其他的软件。其实完全可以在初学python的时候使用jupyter notebook,可以立即得到反馈以及分析错误,可以进步很快!


打开操作

当初安装好之后还不了解,每次打开jupyter notebook都会先弹出一个黑框框,这时候千万别关掉,等一会就能来到网页。另外打开之后也别关掉,使用的时候是一直需要的,因为只有开着才能访问本机web服务器发布的内容。

另外你也可以不通过快捷方式,直接在命令行中直接输入jupyter notebook来打开它。
有些时候,当你插入硬盘或者需要直接在特定的目录下打开jupyter notebook(它的默认打开是在“usr/用户名/”路径下),那你可以在输入命令的后面加上你想要的路径。

1
2
3
jupyter notebook D:\ #打开D盘,于我是我的移动硬盘
jupyter notebook E:\ #我的U盘
jupyter notebook C:\Users\用户名\Desktop #在桌面打开


快捷键操作

在jupyter notebook中可以通过选中cell然后按h的方式查询快捷键。


其他

在jupyter notebook中可以直接使用markdown,这对学习可以起到很大的辅助作用,markdown的基本操作可以看我的另一篇博文markdown笔记:markdown的基本使用
此外,在我的博文anaconda笔记:conda的各种命令行操作中,也介绍了如何将python的虚拟环境添加到jupyter notebook中,欢迎阅读。
VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试,不过我在kernel配置方面似乎还有一些小问题有待解决。

]]>
+

首先强烈安利jupyter notebook,它是一种交互式笔记本,安装anaconda的时候会一并安装,下载VS的时候也可以选择安装。
我是在初学机器学习的时候接触jupyter notebook的,立刻就被它便捷的交互与结果呈现方式所吸引,现在python编程基本不使用其他的软件。其实完全可以在初学python的时候使用jupyter notebook,可以立即得到反馈以及分析错误,可以进步很快!


打开操作

当初安装好之后还不了解,每次打开jupyter notebook都会先弹出一个黑框框,这时候千万别关掉,等一会就能来到网页。另外打开之后也别关掉,使用的时候是一直需要的,因为只有开着才能访问本机web服务器发布的内容。

另外你也可以不通过快捷方式,直接在命令行中直接输入jupyter notebook来打开它。
有些时候,当你插入硬盘或者需要直接在特定的目录下打开jupyter notebook(它的默认打开是在“usr/用户名/”路径下),那你可以在输入命令的后面加上你想要的路径。

1
2
3
jupyter notebook D:\ #打开D盘,于我是我的移动硬盘
jupyter notebook E:\ #我的U盘
jupyter notebook C:\Users\用户名\Desktop #在桌面打开


快捷键操作

在jupyter notebook中可以通过选中cell然后按h的方式查询快捷键。


其他

在jupyter notebook中可以直接使用markdown,这对学习可以起到很大的辅助作用,markdown的基本操作可以看我的另一篇博文markdown笔记:markdown的基本使用
此外,在我的博文anaconda笔记:conda的各种命令行操作中,也介绍了如何将python的虚拟环境添加到jupyter notebook中,欢迎阅读。
VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试,不过我在kernel配置方面似乎还有一些小问题有待解决。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>首先强烈安利jupyter notebook,它是一种交互式笔记本,安装anaconda的时候会一并安装,下载VS的时候也可以选择安装。<br> + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>首先强烈安利jupyter notebook,它是一种交互式笔记本,安装anaconda的时候会一并安装,下载VS的时候也可以选择安装。<br> @@ -2425,13 +2425,13 @@ 2019-09-13T13:11:44.000Z 2020-02-07T08:51:06.737Z -

既然要写技术博客,那么markdown肯定是必备的了,这篇文章就来介绍一下markdown的基本使用操作。

References

电子文献:
https://www.jianshu.com/p/191d1e21f7ed


介绍

Markdown是一种可以使用普通文本编辑器编写的标记语言,其功能比纯文本更强,因此许多程序员用它来写blog。在这里我先推荐一款markdown编辑器——typora,大家可以免费下载使用。


注意

在我刚开始使用markdown的时候总是跳进这个坑,在这里提上来提醒一下,在使用markdown标记后要添加文字时,需要在相应标记后空一格,否则标记也会被当作文本来处理,例如我输入“#####错误”时:

错误

正确的做法是输入“##### 正确”:

正确

一种简单的判别方法就是使用IDE,这样对应的标记就会有语法高亮。


使用

  1. 标题

    话不多说,直接示范:

    1
    2
    3
    4
    5
    6
    # 这是一级标题
    ## 这是二级标题
    ### 这是三级标题
    #### 这是四级标题
    ##### 这是五级标题
    ###### 这是六级标题

    效果如下:

    这是一级标题

    这是二级标题

    这是三级标题

    这是四级标题

    这是五级标题
    这是六级标题
  2. 字体

    还是直接示范:

    1
    2
    3
    4
    **这是加粗的文字**
    *这是倾斜的文字*`
    ***这是斜体加粗的文字***
    ~~这是加删除线的文字~~

    这是加粗的文字
    这是倾斜的文字
    这是斜体加粗的文字
    这是加删除线的文字

  3. 引用

    1
    2
    3
    4
    >我引用
    >>我还引用
    >>>我再引用
    >>>>>>>>>>>>>扶我起来,我还能继续引用!

    我引用

    我还引用

    我再引用

    扶我起来,我还能继续引用!

    引用是可以嵌套的,可以加很多层,我一般使用一个>来表示额外的需要注意的内容。另外,如果想让下一段文字不被引用,需要空一行。

  4. 分割线

    分割线使用三个及以上的-*就可以。
    有时候用---会造成别的文字的格式变化,因此我在使用VScode编辑时,如果看到---被高亮(分割线正常其作用时应该不高亮),就会改用***

    1
    2
    ---
    ***

    效果如下:



  5. 图片

    markdown中添加图片的语法是这样的:

    1
    ![显示在图片下方的文字](图片地址 "图片title")

    其中title可加可不加,它就是鼠标移动到图片上时显示的文字。
    然而我在使用hexo搭建我的个人博客的过程中,遇到了使用上述语法图片却无法显示的情况,因此我改用了下列标签插件:

    1
    {% asset_img xxxxx.xxx 图片下方的名字 %}

    其中xxxxx.xxx只需直接输入图片名称以及格式即可,因为我使用了hexo-asset-image插件,它可以在_posts文件中创建与博文名称相同的对应的文件夹,只需把图片移入即可。注意,这里的图片名中间不能有空格,否则会加载失败(它会以为图片名称到第一个空格为止)。
    其安装命令:npm install hexo-asset-image --save
    也可用cnpm更快地安装:cnpm install hexo-asset-image --save

    补充:后来发现在关于本人中无法使用上述asset_img标签插件来对图片进行插入,故又尝试了![显示在图片下方的文字](图片地址 "图片title")的方法,发现可行!原因可能是之前误用了中文括号导致的。可以参考一下Hexo文章中插入图片的方法

    在插入图片的后面,会留有一小段空白区,看着不舒服的话可以不要回车,即直接在插入图片的语句后面跟进下一段的文字或者图片等,这样行间隙就会小很多。
    其实在hexo中可以直接使用img标签,它会自行处理,并且这样还更方便调整高度和宽度。

    1
    <img src="" width="50%" height="50%">
  6. 超链接

    由于我希望在新的页面打开链接,而似乎markdown本身的语法不支持在新标签页打开链接,因此我推荐直接使用html语言来代替。

    1
    <a href="超链接地址" target="_blank">超链接名</a>
  7. 列表

    • 无序列表

      1
      2
      3
      - 列表内容
      + 列表内容
      * 列表内容
      • 列表内容
      • 列表内容
      • 列表内容
    • 有序列表

      1
      2
      3
      1. 列表内容
      2. 列表内容
      3. 列表内容
      1. 列表内容
      2. 列表内容
      3. 列表内容
        可以看到,上面显示的列表是有嵌套的,方法就是敲三个空格缩进。
  8. 表格

    1
    2
    3
    4
    表头|表头|表头
    ---|:--:|---:
    内容|内容|内容
    内容|内容|内容

    其中第二行的作用分割表头和内容,-有一个就行,为了对齐可多加几个。
    此外文字默认居左,有两种改变方法:
    两边加:表示文字居中。
    右边加:表示文字居右。
    然而我在hexo使用表格时,出现了无法正常转换的问题,因此我改用了如下HTML的表格形式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <table border="1">
    <tr>
    <td>第一行第一列</td>
    <td>第一行第二列</td>
    </tr>
    <tr>
    <td>第二行第一列</td>
    <td>第三行第二列</td>
    </tr>
    </table>

    效果如下:

    第一行第一列第一行第二列
    第二行第一列第二行第二列
  9. 代码

    最后的最后,是我最喜欢ctrl+C+V的代码了。
    单行或句中代码输入方式:

    1
    `来复制我呀`

    显示:
    来复制我呀
    其中“`”在键盘的左上角,我当初找了好久。
    多行代码块的写法就是用上下两对“```”围住。
    好了于是你现在就可以自由的复制粘贴啦。

]]>
+

既然要写技术博客,那么markdown肯定是必备的了,这篇文章就来介绍一下markdown的基本使用操作。

References

电子文献:
https://www.jianshu.com/p/191d1e21f7ed


介绍

Markdown是一种可以使用普通文本编辑器编写的标记语言,其功能比纯文本更强,因此许多程序员用它来写blog。在这里我先推荐一款markdown编辑器——typora,大家可以免费下载使用。


注意

在我刚开始使用markdown的时候总是跳进这个坑,在这里提上来提醒一下,在使用markdown标记后要添加文字时,需要在相应标记后空一格,否则标记也会被当作文本来处理,例如我输入“#####错误”时:

错误

正确的做法是输入“##### 正确”:

正确

一种简单的判别方法就是使用IDE,这样对应的标记就会有语法高亮。


使用

  1. 标题

    话不多说,直接示范:

    1
    2
    3
    4
    5
    6
    # 这是一级标题
    ## 这是二级标题
    ### 这是三级标题
    #### 这是四级标题
    ##### 这是五级标题
    ###### 这是六级标题

    效果如下:

    这是一级标题

    这是二级标题

    这是三级标题

    这是四级标题

    这是五级标题
    这是六级标题
  2. 字体

    还是直接示范:

    1
    2
    3
    4
    **这是加粗的文字**
    *这是倾斜的文字*`
    ***这是斜体加粗的文字***
    ~~这是加删除线的文字~~

    这是加粗的文字
    这是倾斜的文字
    这是斜体加粗的文字
    这是加删除线的文字

  3. 引用

    1
    2
    3
    4
    >我引用
    >>我还引用
    >>>我再引用
    >>>>>>>>>>>>>扶我起来,我还能继续引用!

    我引用

    我还引用

    我再引用

    扶我起来,我还能继续引用!

    引用是可以嵌套的,可以加很多层,我一般使用一个>来表示额外的需要注意的内容。另外,如果想让下一段文字不被引用,需要空一行。

  4. 分割线

    分割线使用三个及以上的-*就可以。
    有时候用---会造成别的文字的格式变化,因此我在使用VScode编辑时,如果看到---被高亮(分割线正常其作用时应该不高亮),就会改用***

    1
    2
    ---
    ***

    效果如下:



  5. 图片

    markdown中添加图片的语法是这样的:

    1
    ![显示在图片下方的文字](图片地址 "图片title")

    其中title可加可不加,它就是鼠标移动到图片上时显示的文字。
    然而我在使用hexo搭建我的个人博客的过程中,遇到了使用上述语法图片却无法显示的情况,因此我改用了下列标签插件:

    1
    {% asset_img xxxxx.xxx 图片下方的名字 %}

    其中xxxxx.xxx只需直接输入图片名称以及格式即可,因为我使用了hexo-asset-image插件,它可以在_posts文件中创建与博文名称相同的对应的文件夹,只需把图片移入即可。注意,这里的图片名中间不能有空格,否则会加载失败(它会以为图片名称到第一个空格为止)。
    其安装命令:npm install hexo-asset-image --save
    也可用cnpm更快地安装:cnpm install hexo-asset-image --save

    补充:后来发现在关于本人中无法使用上述asset_img标签插件来对图片进行插入,故又尝试了![显示在图片下方的文字](图片地址 "图片title")的方法,发现可行!原因可能是之前误用了中文括号导致的。可以参考一下Hexo文章中插入图片的方法

    在插入图片的后面,会留有一小段空白区,看着不舒服的话可以不要回车,即直接在插入图片的语句后面跟进下一段的文字或者图片等,这样行间隙就会小很多。
    其实在hexo中可以直接使用img标签,它会自行处理,并且这样还更方便调整高度和宽度。

    1
    <img src="" width="50%" height="50%">
  6. 超链接

    由于我希望在新的页面打开链接,而似乎markdown本身的语法不支持在新标签页打开链接,因此我推荐直接使用html语言来代替。

    1
    <a href="超链接地址" target="_blank">超链接名</a>
  7. 列表

    • 无序列表

      1
      2
      3
      - 列表内容
      + 列表内容
      * 列表内容
      • 列表内容
      • 列表内容
      • 列表内容
    • 有序列表

      1
      2
      3
      1. 列表内容
      2. 列表内容
      3. 列表内容
      1. 列表内容
      2. 列表内容
      3. 列表内容
        可以看到,上面显示的列表是有嵌套的,方法就是敲三个空格缩进。
  8. 表格

    1
    2
    3
    4
    表头|表头|表头
    ---|:--:|---:
    内容|内容|内容
    内容|内容|内容

    其中第二行的作用分割表头和内容,-有一个就行,为了对齐可多加几个。
    此外文字默认居左,有两种改变方法:
    两边加:表示文字居中。
    右边加:表示文字居右。
    然而我在hexo使用表格时,出现了无法正常转换的问题,因此我改用了如下HTML的表格形式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <table border="1">
    <tr>
    <td>第一行第一列</td>
    <td>第一行第二列</td>
    </tr>
    <tr>
    <td>第二行第一列</td>
    <td>第三行第二列</td>
    </tr>
    </table>

    效果如下:

    第一行第一列第一行第二列
    第二行第一列第二行第二列
  9. 代码

    最后的最后,是我最喜欢ctrl+C+V的代码了。
    单行或句中代码输入方式:

    1
    `来复制我呀`

    显示:
    来复制我呀
    其中“`”在键盘的左上角,我当初找了好久。
    多行代码块的写法就是用上下两对“```”围住。
    好了于是你现在就可以自由的复制粘贴啦。

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>既然要写技术博客,那么markdown肯定是必备的了,这篇文章就来介绍一下markdown的基本使用操作。</p><p><strong>Refe + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>既然要写技术博客,那么markdown肯定是必备的了,这篇文章就来介绍一下markdown的基本使用操作。</p><p><strong>Refe @@ -2451,13 +2451,13 @@ 2019-09-13T07:33:10.000Z 2020-02-26T23:48:20.949Z -

大家好,这是我的第一篇博文,这也是我的第一个自己搭建的网站,既然搭了,那第一篇就讲讲我搭建的过程吧。


安装步骤

  1. 安装node.js

    进入官网
    选择对应系统(我这里用win10),选择LTS(长期支持版本)安装,安装步骤中一直选择next即可。
    安装完后就可以把安装包删除了。
  2. 安装git

    进入官网
    选择对应系统的版本下载,同样也是按默认安装。
    安装成功后,你会在开始菜单中看到git文件夹。 其中Git Bash与linux和mac中的终端类似,它是git自带的程序,提供了linux风格的shell,我们可以在这里面执行相应的命令。

    注意:bash中的复制粘贴操作与linux中类似,ctrl+C用于终止进程,可以用鼠标中键进行粘贴操作。不嫌麻烦的话可以使用ctrl+shift+Cctrl+shift+V进行复制粘贴操作。

  3. 安装hexo

    hexo是一个快速、简洁且高效的博客框架,在这里我们使用hexo来搭建博客。
    首先,新建一个名为“blog”的空文件夹,以后我们的操作都在这个文件夹里进行,可以在bash中使用pwd命令查看当前所处位置。
    创建这个文件夹的目的是万一因为创建的博客出现问题或者不满意想重来等原因可以直接简单地把文件夹删除,也方便了对整个网站本地内容的移动。
    打开新建的文件夹,右键空白处,选择Git Bash Here。 接下来我们输入两行命令来验证node.js是否安装成功。 如出现如图所示结果,则表明安装成功。
    为了提高以后的下载速度,我们需要安装cnpm。cnpm是淘宝的国内镜像,因为npm的服务器位于国外有时可能会影响安装。
    继续在bash中输入npm install -g cnpm --registry=https://registry.npm.taobao.org安装cnpm。
    检验安装是否成功,输入cnpm
    如果输出结果显示如下,说明安装成功。 接下来我们安装hexo,输入命令cnpm install -g hexo-cli
    和上面一样,我们可以用hexo -v来验证是否成功安装hexo,这里就不贴图了。
    接下来我们输入hexo init来初始化建立整个项目。
    你会发现你的文件夹中多了许多文件,你也可以用ls -l命令来看到新增的文件。
  4. 完成本地环境的搭建

    至此,我们已经完成了本地环境的搭建,在这里,我想先介绍hexo中常用的命令。
    hexo n "文章标题":用于创建新的博文(欲删除文章,直接删除md文件并用下面的命令更新即可)。
    hexo s也就是hexo start:hexo会监视文件变动并自动更新,通过所给的地址localhost:4000/就可以直接在本地预览更新后的网站了。这里不需要generate,保存修改后的本地文件后可以直接看到效果。
    部署到远端服务器三步曲:

    1
    2
    3
    hexo clean #清除缓存,网页正常情况下可以忽略此条命令,执行该指令后,会删掉站点根目录下的public文件夹。
    hexo g #generate静态网页(静态网页这里指没有前端后端的网页而不是静止),该命令把md编译为html并存到public文件目录下。
    hexo d #将本地的更改部署到远端服务器(需要一点时间,请过一会再刷新网页)。

    此外,上面最后两步也可以使用hexo g -d直接代替。
    如果出现ERROR Deployer not found: git报错,可以使用npm install --save hexo-deployer-git命令解决。

    注意:由于部署到远端输入密码时密码不可见,有时候会导致部署失败,只有出现INFO Deploy done: git的结果才表明部署成功,否则再次部署重输密码即可。

    现在我们在bash中运行hexo s,打开浏览器,输入localhost:4000/,就可以看到hexo默认创建的页面了。

  5. 部署到远端服务器

    为了让别人能访问到你搭建的网站,我们需要部署到远端服务器。
    这里有两种选择,一种是部署到github上,新建一个repository,然后创建一个xxxxx.github.io域名(这里xxxxx必须为你的github用户名)。
    另一种选择是部署到国内的coding,这是考虑到访问速度的问题,不过我选择的是前者,亲测并没感觉有速度的困扰。
    个人比较推荐用github pages创建个人博客。
    部署这块网上有许多教程,这里不详细解释了,以后有机会补上。
    在部署的时候涉及到对主题配置文件的操作,linux和mac用户可以使用vim进行编辑,不过也可以使用VScode、sublime等代码编辑器进行操作。

    注:为了国内的访问速度,我最后添加了coding/github双线部署,两者的操作方式大同小异。值得注意的是,如果使用的是leancloud的第三方阅读量与评论统计系统,那么还得在leancloud的安全中心中添加coding的web域名。此外,部署到远端之后(特别是第一次)可能要等待几分钟才能访问到更新之后的页面。


创建原因

首先说明,我只是一个刚起步的很萌的萌新,懂得不多,别喷我哈~
步入大二,虽然我是大学才算真正接触编程,但一年多下来我也接触并且学习了不少技术知识。接触的多了、遇到的问题也复杂了起来,导致每次百度到的答案不一定能够解决我遇到的问题。此外,之前在学习编程语言、操作系统、ML、DL等知识的时候,为方便起见利用文本记了些笔记。然而笔记分散在四处,不方便管理与查看,因此就萌生了写博客的想法。由于个人比较喜欢自由DIY,所以没有使用CSDN、博客园等知名技术博客网站。
最后还是非常感谢我们华科的校友程序羊在b站和其他站点上分享的各种经验,我就是通过他的视频来搭建起自己的第一个博客网站的。他的其他视频也给了我很多启迪。
最后,最关键的原因,还是因为今天中秋节有空闲的时间哈哈,祝大家节日快乐!

]]>
+

大家好,这是我的第一篇博文,这也是我的第一个自己搭建的网站,既然搭了,那第一篇就讲讲我搭建的过程吧。


安装步骤

  1. 安装node.js

    进入官网
    选择对应系统(我这里用win10),选择LTS(长期支持版本)安装,安装步骤中一直选择next即可。
    安装完后就可以把安装包删除了。
  2. 安装git

    进入官网
    选择对应系统的版本下载,同样也是按默认安装。
    安装成功后,你会在开始菜单中看到git文件夹。 其中Git Bash与linux和mac中的终端类似,它是git自带的程序,提供了linux风格的shell,我们可以在这里面执行相应的命令。

    注意:bash中的复制粘贴操作与linux中类似,ctrl+C用于终止进程,可以用鼠标中键进行粘贴操作。不嫌麻烦的话可以使用ctrl+shift+Cctrl+shift+V进行复制粘贴操作。

  3. 安装hexo

    hexo是一个快速、简洁且高效的博客框架,在这里我们使用hexo来搭建博客。
    首先,新建一个名为“blog”的空文件夹,以后我们的操作都在这个文件夹里进行,可以在bash中使用pwd命令查看当前所处位置。
    创建这个文件夹的目的是万一因为创建的博客出现问题或者不满意想重来等原因可以直接简单地把文件夹删除,也方便了对整个网站本地内容的移动。
    打开新建的文件夹,右键空白处,选择Git Bash Here。 接下来我们输入两行命令来验证node.js是否安装成功。 如出现如图所示结果,则表明安装成功。
    为了提高以后的下载速度,我们需要安装cnpm。cnpm是淘宝的国内镜像,因为npm的服务器位于国外有时可能会影响安装。
    继续在bash中输入npm install -g cnpm --registry=https://registry.npm.taobao.org安装cnpm。
    检验安装是否成功,输入cnpm
    如果输出结果显示如下,说明安装成功。 接下来我们安装hexo,输入命令cnpm install -g hexo-cli
    和上面一样,我们可以用hexo -v来验证是否成功安装hexo,这里就不贴图了。
    接下来我们输入hexo init来初始化建立整个项目。
    你会发现你的文件夹中多了许多文件,你也可以用ls -l命令来看到新增的文件。
  4. 完成本地环境的搭建

    至此,我们已经完成了本地环境的搭建,在这里,我想先介绍hexo中常用的命令。
    hexo n "文章标题":用于创建新的博文(欲删除文章,直接删除md文件并用下面的命令更新即可)。
    hexo s也就是hexo start:hexo会监视文件变动并自动更新,通过所给的地址localhost:4000/就可以直接在本地预览更新后的网站了。这里不需要generate,保存修改后的本地文件后可以直接看到效果。
    部署到远端服务器三步曲:

    1
    2
    3
    hexo clean #清除缓存,网页正常情况下可以忽略此条命令,执行该指令后,会删掉站点根目录下的public文件夹。
    hexo g #generate静态网页(静态网页这里指没有前端后端的网页而不是静止),该命令把md编译为html并存到public文件目录下。
    hexo d #将本地的更改部署到远端服务器(需要一点时间,请过一会再刷新网页)。

    此外,上面最后两步也可以使用hexo g -d直接代替。
    如果出现ERROR Deployer not found: git报错,可以使用npm install --save hexo-deployer-git命令解决。

    注意:由于部署到远端输入密码时密码不可见,有时候会导致部署失败,只有出现INFO Deploy done: git的结果才表明部署成功,否则再次部署重输密码即可。

    现在我们在bash中运行hexo s,打开浏览器,输入localhost:4000/,就可以看到hexo默认创建的页面了。

  5. 部署到远端服务器

    为了让别人能访问到你搭建的网站,我们需要部署到远端服务器。
    这里有两种选择,一种是部署到github上,新建一个repository,然后创建一个xxxxx.github.io域名(这里xxxxx必须为你的github用户名)。
    另一种选择是部署到国内的coding,这是考虑到访问速度的问题,不过我选择的是前者,亲测并没感觉有速度的困扰。
    个人比较推荐用github pages创建个人博客。
    部署这块网上有许多教程,这里不详细解释了,以后有机会补上。
    在部署的时候涉及到对主题配置文件的操作,linux和mac用户可以使用vim进行编辑,不过也可以使用VScode、sublime等代码编辑器进行操作。

    注:为了国内的访问速度,我最后添加了coding/github双线部署,两者的操作方式大同小异。值得注意的是,如果使用的是leancloud的第三方阅读量与评论统计系统,那么还得在leancloud的安全中心中添加coding的web域名。此外,部署到远端之后(特别是第一次)可能要等待几分钟才能访问到更新之后的页面。


创建原因

首先说明,我只是一个刚起步的很萌的萌新,懂得不多,别喷我哈~
步入大二,虽然我是大学才算真正接触编程,但一年多下来我也接触并且学习了不少技术知识。接触的多了、遇到的问题也复杂了起来,导致每次百度到的答案不一定能够解决我遇到的问题。此外,之前在学习编程语言、操作系统、ML、DL等知识的时候,为方便起见利用文本记了些笔记。然而笔记分散在四处,不方便管理与查看,因此就萌生了写博客的想法。由于个人比较喜欢自由DIY,所以没有使用CSDN、博客园等知名技术博客网站。
最后还是非常感谢我们华科的校友程序羊在b站和其他站点上分享的各种经验,我就是通过他的视频来搭建起自己的第一个博客网站的。他的其他视频也给了我很多启迪。
最后,最关键的原因,还是因为今天中秋节有空闲的时间哈哈,祝大家节日快乐!

]]>
- <!-- build time:Sat May 02 2020 23:29:04 GMT+0800 (GMT+08:00) --><p>大家好,这是我的第一篇博文,这也是我的第一个自己搭建的网站,既然搭了,那第一篇就讲讲我搭建的过程吧。</p><hr><h1 id="安装步骤"> + <!-- build time:Sun May 03 2020 09:40:01 GMT+0800 (GMT+08:00) --><p>大家好,这是我的第一篇博文,这也是我的第一个自己搭建的网站,既然搭了,那第一篇就讲讲我搭建的过程吧。</p><hr><h1 id="安装步骤"> diff --git a/baidusitemap.xml b/baidusitemap.xml index 4bc5e79f3..3cdae148e 100644 --- a/baidusitemap.xml +++ b/baidusitemap.xml @@ -1,10 +1,13 @@ - https://gsy00517.github.io/matlab20200502225129/ - 2020-05-02 + https://gsy00517.github.io/logisim20200107121101/ + 2020-05-03 https://gsy00517.github.io/vot-toolkit20200215185238/ + 2020-05-03 + + https://gsy00517.github.io/matlab20200502225129/ 2020-05-02 https://gsy00517.github.io/computer-vision20200215214240/ @@ -249,9 +252,6 @@ https://gsy00517.github.io/circuit20200107222439/ 2020-01-19 - - https://gsy00517.github.io/logisim20200107121101/ - 2020-01-07 https://gsy00517.github.io/game20191007172952/ 2019-12-01 diff --git a/calculus20191007184856/index.html b/calculus20191007184856/index.html index 3443c66fc..1300a7361 100644 --- a/calculus20191007184856/index.html +++ b/calculus20191007184856/index.html @@ -1,4 +1,4 @@ -calculus笔记:分部积分表格法 | 高深远的博客

calculus笔记:分部积分表格法

假期里想着不能让b站收藏夹里的学习资源一直吃灰,于是又刷了一遍b站的收藏夹。碰巧就看到了自己之前收藏的一种积分方法,那么这篇文章就来搬运一下这种方法的计算流程。


表格法

事实上,这种方法说白了还是分部积分法,但使用起来却要方便好多。我们直接看例子:
求解$ \int \left ( x^{2}+x \right )e^{x}dx $。

  1. 画一个两行的表格。把多项式部分写在第一行,然后把剩余的部分写在第二行。
    $ x^{2}+x\ $
    $ e^{x}\ $
  2. 接下来,我们对第一行求导,直到导数为零为止。对第二行积分,直到与第一行的0对齐为止。
    $ x^{2}+x\ $$ 2x+1\ $20
    $ e^{x}\ $$ e^{x}\ $$ e^{x}\ $$ e^{x}\ $
  3. 第三步就是交叉相乘,在本题即为第一行第一列与第二行第二列相乘,第一行第二列与第二行第三列相乘,第一行第三列与第二行第四列相乘。要注意的是,这里的交叉相乘还需要带符号,依次为正负正负正…以此类推。最后,将相乘结果相加,整理即可得到最终的解。

    注意:别忘了加上常数C。

下面再来看一个例子熟悉一下:
求解$ \int xsinxdx $。
画表格:
$ x\ $$ 1\ $$ 0\ $
$ sinx\ $$ -cosx\ $$ -sinx\ $

求解:

其实b站上还是有挺多这样的干货的,此生无悔入b站!


其它运算终止情况

看完上面的部分,细心的你肯定会想到以上的方法并不普适,仅仅适用于导数能求导至零及含有多项式因式的情况。因此,为了能更灵活地运用分部积分表格法,下面补充其它两种运算可以终止的情况。

  1. 第一行出现零元素

    这就是上面所说的含多项式的情况,也一并列写在这里,方便总览归纳。
  2. 某列函数的乘积(或它的常数倍)等于第一列

    按照分部积分的一般做法,当出现之后的某一项恰好是原来积分或者是原来积分的常数倍时,计算进入循环。这时就可以把两者移到等式的同一侧,计算出结果,这在表格法的分部积分中也是类似的。
    来看看例子:求解$ \int e^{3x}sin2xdx $。
    $ e^{3x}\ $$ 3e^{3x}\ $$ 9e^{3x}\ $
    $ sin2x\ $$ -\frac{cos2x}{2}\ $$ -\frac{sin2x}{4}\ $

    可见,第三列的乘积和第一列的乘积相差一个常数(这里是$ -\frac{9}{4} $),因此仿照之前的方法交叉相乘列出积分:移项化简可得:即为所求。
    看完这种情况,你一定会敏锐地发现,其实分部积分表格法本质上和一般的分部积分法一模一样,不过的确在使用上还是有一定的优势的。
  3. 某列的两个函数乘积(记为$ f(x) $)是一个容易计算的积分

    这种情况下,先把之前的项用之前的方法类似列出,再在结果后加上不定积分$ (-1)^{k-1}\int f(x)dx $。
    来看例子:求解$ \int x^{2}arctanxdx $。
    $ arctanx\ $$ \frac{1}{1+x^{2}}\ $
    $ x^{2}\ $$ \frac{1}{3}x^{3}\ $

    可得解:另外,当表中的第一行的某列出现多项之和,而再求导无法改变该函数或者该函数中某一项的属性,则终止表格,后再重新组合,另建表格求解。这种情况一般不会出现在题目中,如遇到再做补充。

碰到底线咯 后面没有啦

本文标题:calculus笔记:分部积分表格法

文章作者:高深远

发布时间:2019年10月07日 - 18:48

最后更新:2019年11月03日 - 13:44

原始链接:https://gsy00517.github.io/calculus20191007184856/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
calculus笔记:分部积分表格法 | 高深远的博客

calculus笔记:分部积分表格法

假期里想着不能让b站收藏夹里的学习资源一直吃灰,于是又刷了一遍b站的收藏夹。碰巧就看到了自己之前收藏的一种积分方法,那么这篇文章就来搬运一下这种方法的计算流程。


表格法

事实上,这种方法说白了还是分部积分法,但使用起来却要方便好多。我们直接看例子:
求解$ \int \left ( x^{2}+x \right )e^{x}dx $。

  1. 画一个两行的表格。把多项式部分写在第一行,然后把剩余的部分写在第二行。
    $ x^{2}+x\ $
    $ e^{x}\ $
  2. 接下来,我们对第一行求导,直到导数为零为止。对第二行积分,直到与第一行的0对齐为止。
    $ x^{2}+x\ $$ 2x+1\ $20
    $ e^{x}\ $$ e^{x}\ $$ e^{x}\ $$ e^{x}\ $
  3. 第三步就是交叉相乘,在本题即为第一行第一列与第二行第二列相乘,第一行第二列与第二行第三列相乘,第一行第三列与第二行第四列相乘。要注意的是,这里的交叉相乘还需要带符号,依次为正负正负正…以此类推。最后,将相乘结果相加,整理即可得到最终的解。

    注意:别忘了加上常数C。

下面再来看一个例子熟悉一下:
求解$ \int xsinxdx $。
画表格:
$ x\ $$ 1\ $$ 0\ $
$ sinx\ $$ -cosx\ $$ -sinx\ $

求解:

其实b站上还是有挺多这样的干货的,此生无悔入b站!


其它运算终止情况

看完上面的部分,细心的你肯定会想到以上的方法并不普适,仅仅适用于导数能求导至零及含有多项式因式的情况。因此,为了能更灵活地运用分部积分表格法,下面补充其它两种运算可以终止的情况。

  1. 第一行出现零元素

    这就是上面所说的含多项式的情况,也一并列写在这里,方便总览归纳。
  2. 某列函数的乘积(或它的常数倍)等于第一列

    按照分部积分的一般做法,当出现之后的某一项恰好是原来积分或者是原来积分的常数倍时,计算进入循环。这时就可以把两者移到等式的同一侧,计算出结果,这在表格法的分部积分中也是类似的。
    来看看例子:求解$ \int e^{3x}sin2xdx $。
    $ e^{3x}\ $$ 3e^{3x}\ $$ 9e^{3x}\ $
    $ sin2x\ $$ -\frac{cos2x}{2}\ $$ -\frac{sin2x}{4}\ $

    可见,第三列的乘积和第一列的乘积相差一个常数(这里是$ -\frac{9}{4} $),因此仿照之前的方法交叉相乘列出积分:移项化简可得:即为所求。
    看完这种情况,你一定会敏锐地发现,其实分部积分表格法本质上和一般的分部积分法一模一样,不过的确在使用上还是有一定的优势的。
  3. 某列的两个函数乘积(记为$ f(x) $)是一个容易计算的积分

    这种情况下,先把之前的项用之前的方法类似列出,再在结果后加上不定积分$ (-1)^{k-1}\int f(x)dx $。
    来看例子:求解$ \int x^{2}arctanxdx $。
    $ arctanx\ $$ \frac{1}{1+x^{2}}\ $
    $ x^{2}\ $$ \frac{1}{3}x^{3}\ $

    可得解:另外,当表中的第一行的某列出现多项之和,而再求导无法改变该函数或者该函数中某一项的属性,则终止表格,后再重新组合,另建表格求解。这种情况一般不会出现在题目中,如遇到再做补充。

碰到底线咯 后面没有啦

本文标题:calculus笔记:分部积分表格法

文章作者:高深远

发布时间:2019年10月07日 - 18:48

最后更新:2019年11月03日 - 13:44

原始链接:https://gsy00517.github.io/calculus20191007184856/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
分类 | 高深远的博客
0%
分类 | 高深远的博客
0%
分类: 人工智能 | 高深远的博客分类: 人工智能 | 高深远的博客分类: 人工智能 | 高深远的博客分类: 人工智能 | 高深远的博客分类: 人工智能 | 高深远的博客分类: 人工智能 | 高深远的博客分类: 人工智能 | 高深远的博客分类: 人工智能 | 高深远的博客分类: 操作和使用 | 高深远的博客分类: 操作和使用 | 高深远的博客分类: 操作和使用 | 高深远的博客分类: 操作和使用 | 高深远的博客分类: 操作和使用 | 高深远的博客分类: 操作和使用 | 高深远的博客分类: 游戏 | 高深远的博客分类: 游戏 | 高深远的博客分类: 环境配置 | 高深远的博客分类: 环境配置 | 高深远的博客分类: 知识点与小技巧 | 高深远的博客分类: 知识点与小技巧 | 高深远的博客分类: 程序与设计 | 高深远的博客分类: 程序与设计 | 高深远的博客分类: 程序与设计 | 高深远的博客分类: 程序与设计 | 高深远的博客分类: 英语 | 高深远的博客分类: 英语 | 高深远的博客circuit笔记:有线双工对讲机的电子线路设计 | 高深远的博客

circuit笔记:有线双工对讲机的电子线路设计

模拟电路实验是我进入大学本科以来第一个付出大量课外时间的实验课,其最后的大项目是让我们自行设计一个电路,而我们小组选择的是有线双工对讲机。今天我就简单对我们小组的设计方案做一个整理。在这里还是非常感谢组员们的共同努力和巨大帮助!

References

电子文献:
http://www.ttic.cc/file/TDA1013B_76329.html.com
https://wenku.baidu.com/view/c8b016e7ed630b1c58eeb520.html
https://tech.hqew.com/fangan_1909806


设计目的

有线对讲机是用导线直接连接进行通话,而双工通信则是像电话机一样同时进行双方的“听”和“讲”。此外,我们希望可以具有音量可调节、消侧音等一些对讲机需要的功能。


设计思路

  1. 利用驻极体话筒将声音信号转化为微弱的电信号。
  2. 通过反相比例放大器将微弱的电信号放大。
  3. 利用相位抵消法实现消侧音。
  4. 只使用一根传输线进行信号互传。
  5. 利用电压跟随器避免远距离导线传输时衰减过大的问题。
  6. 添加了低通滤波器电路,滤去高频的噪音信号。
  7. 使用TDA1013B进行功率放大并将信号传输到扬声器。
  8. 最后由扬声器将电信号转化成声音信号,发出声音。

设计有线双工对讲机的思路可以用如下所示的系统图表示。主要由弱声音采集、前置运算放大器、消侧音电路、减小信号衰减电路、低通滤波器电路、功率放大电路、扬声器等模块组成。


基本原理

  1. 驻极体话筒

    话筒的基本结构由一片单面涂有金属的驻极体薄膜与一个上面有若干小孔的金属电极(背称为背电极)构成。驻极体面与背电极相对,中间有一个极小的空气隙,形成一个以空气隙和驻极体作绝缘介质,以背电极和驻极体上的金属层作为两个电极构成一个平板电容器。电容的两极之间有输出电极。
    由于驻极体薄膜上分布有自由电荷,当声波引起驻极体薄膜振动而产生位移时;改变了电容两极板之间的距离,从而引起电容的容量发生变化,由于驻极体上的电荷数始终保持恒定,根据公式:Q =CU 所以当C变化时必然引起电容器两端电压U的变化,从而输出电信号,实现声电的变换。
    不管是源极输出或漏极输出,驻极体话筒必须提供直流电压才能工作,因为它内部装有场效应管。
  2. 反向比例放大

    相比例放大电路的原理如上图所示,输出信号电压增益为R2与R1之比,相位反相变化180°(后面会再次反相)。在本实验中,我们将两个电阻的比值调整在10~100之间,即放大比例为10~100。
  3. 消侧音

    在模拟音频收发信号共用一个信道的对讲系统中,为减小侧音对通话效果的影响,防止侧音的干扰,所有对讲设备均需增加消侧音电路。一方面让音频发送信号按一定比例出现在传输线上,另一方面让本方音频接收电路获得的信号足够小,不至于说话者从己方喇叭听到自己的声音,提高通话的质量。 在上图所示的串联分压电路中,R1、R2为纯电阻,v1、v2为输入电压,vo为输出电压,据叠加定理:令vo=0,则v1R2+v2R1=0,即:特别地,当R1=R2时,v1=-v2。
    由上式可见,欲使vo=0,v1,v2须满足2个条件:
    1. 每个频率分量的相位相反。
    2. 每个频率分量幅度呈一定比例且比例相同。
      下面是该方法的一种实现方式的电路图: 根据文首所列的参考资料,我们找到了另一种原理相同且成本更低的实现方式,如下图所示。三极管发射极和集电极的信号反相且幅度相同,可以相叠加后将信号消去,一个三极管的作用相当于上述方法中的U1和U2。图中三级管的偏置电路没有画出,C1和C2将直流分量同传输线隔离开。需要特别提出的是,如果可调电阻P1足够大,从而对三极管的偏置影响足够小,可将C2去掉,可调电阻P1直接和三极管的c,e极并联。 需要注意的是,在这里传送到对方的信号的相位再次反相,与原始信号相一致。
      实验中,三极管采用9013,电位器采用104。为了使三极管正常工作,在三极管B端由R1和R3分压,使三极管的静态工作点VCQ≥4.5V,则令VEQ=VCC-VCQ,需要VR2=VR4,因此选R2=R4=1kΩ,则由IEQ=ICQ得VR2=VR4。由于有VBE=0.7V,则VBQ=VEQ+0.7,VBQ+VR1=VCC=12V。此时若ICQ=4mA,经计算,则R1与R3之比约为1.5。我们最终选取R1=6.7kΩ,R3=4.7kΩ来分压。
  4. 减小信号衰减

    由于我们的目标是实现长导线远程传输信号,因此导线上的电阻是不可忽略的,这就导致了信号衰减的问题。在弱电的情况下,解决导线上信号衰减的方法有选取更优质的导线、改用电流信号输出、增大接收端的输入电阻等。
    利用电压跟随器输入电阻高的特性,我们决定在信号接收的两端分别添加一个电压跟随器(如下图所示)来抑制信号传输的衰减。
  5. 低通滤波

    人耳可以听到20HZ到20kHZ的音频信号,而人正常对话所发出的声音频率约为300HZ—3000HZ,频率较低,因此我们设计一个低通语音滤波器来滤除杂音,提高声音清晰度。
    我们的目的是设计一个低频增益A0=2,Q≈1(品质因数,越小则通带或阻带越平坦,电路的稳定性越好), fH=3kHz,图如下所示是一个二阶压控电压源低通滤波器。 根据该电路低频增益A0=K=1+R27/R28=2,可知R27=R28,因此我们选R27=R28=10kΩ。
    根据fC=ωC/2π,当R25=R26=R,C10=C12=C时,有ωC=1/(RC)。
    因此fC=1/2πRC。由所需上限截止频率fH为3kHz,我们选择C=0.01μF,算出R=1/(2πfC) ≈5.3kΩ。这里我们选用4.7kΩ的电阻,可实现近似的功能。
  6. 使用TDA1013B功率放大

    在最后的输出之前,我们需要对信号进行功率的放大。在查阅相关资料之后,我们发现TDA1013B比较符合我们的功能需求。
    TDA1013B是一个音频功率放大器集成电路,内部具有按对数曲线变化的直流音量控制电路,控制范围可达80dB,它具有很宽的电源电压范围(10V~40V),输出功率位4W~10W,是理想的音频功率放大器。
    根据文首列出的数据手册,TDA1013B的伴音电路连接方式如下。 其中各个引脚的功能分别是:
    1脚:电源地。
    2脚:放大器输出,这里作伴音输出。
    3脚:电源。
    4脚:电源。
    5脚:功放输入。
    6脚:控制单元输出。
    7脚:控制电压,这里可用于音量控制。
    8脚:控制单元输入,这里输入音频。
    9脚:信号地。
    通过分析和查阅资料(见本文参考),我们确定了芯片的连接方式如下图所示。

设计实现

如下是我们的仿真总电路图(还缺少最后的两级功率放大电路)。我们使用的是multisim14.0,由于软件的库中没有TDA1013B芯片,且声音信号难以在仿真软件中模拟,因此我们在仿真模拟阶段选择分模块检验功能的实现效果。

我们在第一级放大电路前后使用示波器检测放大效果,我们输入100mV峰峰值、1000Hz频率的交流信号,得到输出如下,其中通道A为放大之后的的信号,通道B显示的是放大之前的信号。

我们还检测了声音低通滤波器的功能实现情况,我们将对应的模块分离出来,利用波特测试仪画出该电路的波特图,结果如下所示。分别使用了对数和线性的横坐标轴(频率),且分别设扫描上限为100kHz和20kHz,由图易知,在3kHz左右处,增益开始下降,基本符合我们的设计要求。


碰到底线咯 后面没有啦

本文标题:circuit笔记:有线双工对讲机的电子线路设计

文章作者:高深远

发布时间:2020年01月07日 - 22:24

最后更新:2020年01月19日 - 08:23

原始链接:https://gsy00517.github.io/circuit20200107222439/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
circuit笔记:有线双工对讲机的电子线路设计 | 高深远的博客

circuit笔记:有线双工对讲机的电子线路设计

模拟电路实验是我进入大学本科以来第一个付出大量课外时间的实验课,其最后的大项目是让我们自行设计一个电路,而我们小组选择的是有线双工对讲机。今天我就简单对我们小组的设计方案做一个整理。在这里还是非常感谢组员们的共同努力和巨大帮助!

References

电子文献:
http://www.ttic.cc/file/TDA1013B_76329.html.com
https://wenku.baidu.com/view/c8b016e7ed630b1c58eeb520.html
https://tech.hqew.com/fangan_1909806


设计目的

有线对讲机是用导线直接连接进行通话,而双工通信则是像电话机一样同时进行双方的“听”和“讲”。此外,我们希望可以具有音量可调节、消侧音等一些对讲机需要的功能。


设计思路

  1. 利用驻极体话筒将声音信号转化为微弱的电信号。
  2. 通过反相比例放大器将微弱的电信号放大。
  3. 利用相位抵消法实现消侧音。
  4. 只使用一根传输线进行信号互传。
  5. 利用电压跟随器避免远距离导线传输时衰减过大的问题。
  6. 添加了低通滤波器电路,滤去高频的噪音信号。
  7. 使用TDA1013B进行功率放大并将信号传输到扬声器。
  8. 最后由扬声器将电信号转化成声音信号,发出声音。

设计有线双工对讲机的思路可以用如下所示的系统图表示。主要由弱声音采集、前置运算放大器、消侧音电路、减小信号衰减电路、低通滤波器电路、功率放大电路、扬声器等模块组成。


基本原理

  1. 驻极体话筒

    话筒的基本结构由一片单面涂有金属的驻极体薄膜与一个上面有若干小孔的金属电极(背称为背电极)构成。驻极体面与背电极相对,中间有一个极小的空气隙,形成一个以空气隙和驻极体作绝缘介质,以背电极和驻极体上的金属层作为两个电极构成一个平板电容器。电容的两极之间有输出电极。
    由于驻极体薄膜上分布有自由电荷,当声波引起驻极体薄膜振动而产生位移时;改变了电容两极板之间的距离,从而引起电容的容量发生变化,由于驻极体上的电荷数始终保持恒定,根据公式:Q =CU 所以当C变化时必然引起电容器两端电压U的变化,从而输出电信号,实现声电的变换。
    不管是源极输出或漏极输出,驻极体话筒必须提供直流电压才能工作,因为它内部装有场效应管。
  2. 反向比例放大

    相比例放大电路的原理如上图所示,输出信号电压增益为R2与R1之比,相位反相变化180°(后面会再次反相)。在本实验中,我们将两个电阻的比值调整在10~100之间,即放大比例为10~100。
  3. 消侧音

    在模拟音频收发信号共用一个信道的对讲系统中,为减小侧音对通话效果的影响,防止侧音的干扰,所有对讲设备均需增加消侧音电路。一方面让音频发送信号按一定比例出现在传输线上,另一方面让本方音频接收电路获得的信号足够小,不至于说话者从己方喇叭听到自己的声音,提高通话的质量。 在上图所示的串联分压电路中,R1、R2为纯电阻,v1、v2为输入电压,vo为输出电压,据叠加定理:令vo=0,则v1R2+v2R1=0,即:特别地,当R1=R2时,v1=-v2。
    由上式可见,欲使vo=0,v1,v2须满足2个条件:
    1. 每个频率分量的相位相反。
    2. 每个频率分量幅度呈一定比例且比例相同。
      下面是该方法的一种实现方式的电路图: 根据文首所列的参考资料,我们找到了另一种原理相同且成本更低的实现方式,如下图所示。三极管发射极和集电极的信号反相且幅度相同,可以相叠加后将信号消去,一个三极管的作用相当于上述方法中的U1和U2。图中三级管的偏置电路没有画出,C1和C2将直流分量同传输线隔离开。需要特别提出的是,如果可调电阻P1足够大,从而对三极管的偏置影响足够小,可将C2去掉,可调电阻P1直接和三极管的c,e极并联。 需要注意的是,在这里传送到对方的信号的相位再次反相,与原始信号相一致。
      实验中,三极管采用9013,电位器采用104。为了使三极管正常工作,在三极管B端由R1和R3分压,使三极管的静态工作点VCQ≥4.5V,则令VEQ=VCC-VCQ,需要VR2=VR4,因此选R2=R4=1kΩ,则由IEQ=ICQ得VR2=VR4。由于有VBE=0.7V,则VBQ=VEQ+0.7,VBQ+VR1=VCC=12V。此时若ICQ=4mA,经计算,则R1与R3之比约为1.5。我们最终选取R1=6.7kΩ,R3=4.7kΩ来分压。
  4. 减小信号衰减

    由于我们的目标是实现长导线远程传输信号,因此导线上的电阻是不可忽略的,这就导致了信号衰减的问题。在弱电的情况下,解决导线上信号衰减的方法有选取更优质的导线、改用电流信号输出、增大接收端的输入电阻等。
    利用电压跟随器输入电阻高的特性,我们决定在信号接收的两端分别添加一个电压跟随器(如下图所示)来抑制信号传输的衰减。
  5. 低通滤波

    人耳可以听到20HZ到20kHZ的音频信号,而人正常对话所发出的声音频率约为300HZ—3000HZ,频率较低,因此我们设计一个低通语音滤波器来滤除杂音,提高声音清晰度。
    我们的目的是设计一个低频增益A0=2,Q≈1(品质因数,越小则通带或阻带越平坦,电路的稳定性越好), fH=3kHz,图如下所示是一个二阶压控电压源低通滤波器。 根据该电路低频增益A0=K=1+R27/R28=2,可知R27=R28,因此我们选R27=R28=10kΩ。
    根据fC=ωC/2π,当R25=R26=R,C10=C12=C时,有ωC=1/(RC)。
    因此fC=1/2πRC。由所需上限截止频率fH为3kHz,我们选择C=0.01μF,算出R=1/(2πfC) ≈5.3kΩ。这里我们选用4.7kΩ的电阻,可实现近似的功能。
  6. 使用TDA1013B功率放大

    在最后的输出之前,我们需要对信号进行功率的放大。在查阅相关资料之后,我们发现TDA1013B比较符合我们的功能需求。
    TDA1013B是一个音频功率放大器集成电路,内部具有按对数曲线变化的直流音量控制电路,控制范围可达80dB,它具有很宽的电源电压范围(10V~40V),输出功率位4W~10W,是理想的音频功率放大器。
    根据文首列出的数据手册,TDA1013B的伴音电路连接方式如下。 其中各个引脚的功能分别是:
    1脚:电源地。
    2脚:放大器输出,这里作伴音输出。
    3脚:电源。
    4脚:电源。
    5脚:功放输入。
    6脚:控制单元输出。
    7脚:控制电压,这里可用于音量控制。
    8脚:控制单元输入,这里输入音频。
    9脚:信号地。
    通过分析和查阅资料(见本文参考),我们确定了芯片的连接方式如下图所示。

设计实现

如下是我们的仿真总电路图(还缺少最后的两级功率放大电路)。我们使用的是multisim14.0,由于软件的库中没有TDA1013B芯片,且声音信号难以在仿真软件中模拟,因此我们在仿真模拟阶段选择分模块检验功能的实现效果。

我们在第一级放大电路前后使用示波器检测放大效果,我们输入100mV峰峰值、1000Hz频率的交流信号,得到输出如下,其中通道A为放大之后的的信号,通道B显示的是放大之前的信号。

我们还检测了声音低通滤波器的功能实现情况,我们将对应的模块分离出来,利用波特测试仪画出该电路的波特图,结果如下所示。分别使用了对数和线性的横坐标轴(频率),且分别设扫描上限为100kHz和20kHz,由图易知,在3kHz左右处,增益开始下降,基本符合我们的设计要求。


碰到底线咯 后面没有啦

本文标题:circuit笔记:有线双工对讲机的电子线路设计

文章作者:高深远

发布时间:2020年01月07日 - 22:24

最后更新:2020年01月19日 - 08:23

原始链接:https://gsy00517.github.io/circuit20200107222439/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:Peak-to-Sidelobe Ratio应用于目标跟踪 | 高深远的博客

computer vision笔记:Peak-to-Sidelobe Ratio应用于目标跟踪

在Visual Object Tracking using Adaptive Correlation Filters一文中,我看到这样一句话:“The Peak-to-Sidelobe Ratio(PSR), which measures the strength of a correlation peak, can be used to detect occlusions or tracking failure, to stop the online update, and to reacquire the track if the object reappears with a similar appearance.”其大意为:用来衡量相关峰值强度的Peak-to-Sidelobe Ratio可以用于检测遮挡或者跟踪失误、停止实时更新和在相似外观再次出现时重新获取跟踪路径。我读完在想:这个PSR是什么?这么有用。结果百度了半天翻了几页没发现有任何有关它介绍或者解释的文章。于是看了一些英文文献,将自己的一些浅见写下来。

注:近期发现在DCF类的tracker中PSR得到了比较广泛的应用,所以原谅我当初论文看得少哈~

References

电子文献:
https://baike.baidu.com/item/pslr/19735601

参考文献:
[1]Understanding and Diagnosing Visual Tracking Systems
[2]Visual Object Tracking using Adaptive Correlation Filters
[3]Adaptive Model Update via Fusing Peak-to-sidelobe Ratio and Mean Frame Difference for Visual Tracking
[4]Multi-Cue Correlation Filters for Robust Visual Tracking


What

由于没有找到任何中文翻译,我只能取一个相近的(感觉指的差不多)。在百度百科中,我找到了如下解释:PSLR,峰值旁瓣比,peak side lobe ratio,定义为主瓣峰值强度对于最强旁瓣的峰值强度之比,多用于雷达信号脉冲压缩后对信号的评估。

注意:百度百科内的名词与我遇到的相差了一个“to”,且缩写也不同。

上面是百度百科内的一个配图,根据百科解释,最高的主瓣与第二高的主瓣之差,即是PLSR(峰值旁瓣比),在这里大小为-13.4dB。
此外,我还找到了一个称为峰均比(PAPR,peak-to-average power ratio)的概念,它等于波形的振幅和其有效值(RMS)之比,主要是针对功率的,这里就不细说了。
后来,我在一篇光学期刊的文章上找到了一个比较可靠的翻译,该文献中有一个名为“峰旁比”的名词,且文献内容与目标追踪相关。因此我暂且称其为峰旁比吧。个人觉得比峰值旁瓣比简洁且好听。


Why

其实中文翻译并不重要,重要的是它的作用。
在查阅文献的过程中,我看到了这样一个公式:

其中$P_{sr}^{t}$是此时第t帧的峰旁比,$f_{t}$是对于第t帧分类器预测的响应,$\mu _{t}$和$\sigma _{t}$分别是响应图$f$的均值和方差。
根据这种计算方法,我们可以大概分析一下峰旁比的作用。当初看到时,我觉得它与我在物理实验中做过的音叉共振实验中的品质因数有相似之处(品质因数$Q=\frac{f_{0}}{f_{2}-f_{1}}$)。
由公式可知,当响应中的峰值较高,且响应分布相对而言集中在峰值及周围时,峰旁比就会较高;反之,峰旁比就会较低。
那么什么时候会造成峰旁比较低呢?根据论文描述可以获得提示,当遇到遮挡,或者目标跟丢即响应区内不含目标主体时,就不会出现一个那么明显的峰值响应,同时响应也会较为分散了,此时分母较大、分子较小,峰旁比就会变低。
下面也是Visual Object Tracking using Adaptive Correlation Filters中的一张figure(这篇文章提出了MOSSE),我们需要的是a much stronger peak which translates into less drift and fewer dropped tracks。看了这个想必应该知道峰旁比发挥的作用和大致原因了。

和上面的峰值旁瓣比、峰均比相比较,显然峰旁比的定义能更好地表征响应的集中程度。


How

由峰旁比定义所得出的性质可知,峰旁比可以作为模型预测定位准确性和可信度的判据。我们可以利用峰旁比来调整Model Updater(The model updater controls the strategy and frequency of updating the observation model. It has to strike a balance between model adaptation and drift.)
我的想法是,我们可以设置一个threshold,当峰旁比小于这个threshold时,表示模型未能准确定位到目标(可信度较低),这时我们可以停止模型的更新,或者通过减小学习率等方法减慢模型的更新速度以防止模型受背景或者遮挡物较大影响,而当目标再次出现时难以复原导致最终完全跟丢的问题;而当峰旁比大于这个threshold时,我们可以实时更新模型,或者运用较大的学习率(也可以根据峰旁比将学习率划分成几个等级)。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:Peak-to-Sidelobe Ratio应用于目标跟踪

文章作者:高深远

发布时间:2020年01月18日 - 21:39

最后更新:2020年02月23日 - 21:15

原始链接:https://gsy00517.github.io/computer-vision20200118213942/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:Peak-to-Sidelobe Ratio应用于目标跟踪 | 高深远的博客

computer vision笔记:Peak-to-Sidelobe Ratio应用于目标跟踪

在Visual Object Tracking using Adaptive Correlation Filters一文中,我看到这样一句话:“The Peak-to-Sidelobe Ratio(PSR), which measures the strength of a correlation peak, can be used to detect occlusions or tracking failure, to stop the online update, and to reacquire the track if the object reappears with a similar appearance.”其大意为:用来衡量相关峰值强度的Peak-to-Sidelobe Ratio可以用于检测遮挡或者跟踪失误、停止实时更新和在相似外观再次出现时重新获取跟踪路径。我读完在想:这个PSR是什么?这么有用。结果百度了半天翻了几页没发现有任何有关它介绍或者解释的文章。于是看了一些英文文献,将自己的一些浅见写下来。

注:近期发现在DCF类的tracker中PSR得到了比较广泛的应用,所以原谅我当初论文看得少哈~

References

电子文献:
https://baike.baidu.com/item/pslr/19735601

参考文献:
[1]Understanding and Diagnosing Visual Tracking Systems
[2]Visual Object Tracking using Adaptive Correlation Filters
[3]Adaptive Model Update via Fusing Peak-to-sidelobe Ratio and Mean Frame Difference for Visual Tracking
[4]Multi-Cue Correlation Filters for Robust Visual Tracking


What

由于没有找到任何中文翻译,我只能取一个相近的(感觉指的差不多)。在百度百科中,我找到了如下解释:PSLR,峰值旁瓣比,peak side lobe ratio,定义为主瓣峰值强度对于最强旁瓣的峰值强度之比,多用于雷达信号脉冲压缩后对信号的评估。

注意:百度百科内的名词与我遇到的相差了一个“to”,且缩写也不同。

上面是百度百科内的一个配图,根据百科解释,最高的主瓣与第二高的主瓣之差,即是PLSR(峰值旁瓣比),在这里大小为-13.4dB。
此外,我还找到了一个称为峰均比(PAPR,peak-to-average power ratio)的概念,它等于波形的振幅和其有效值(RMS)之比,主要是针对功率的,这里就不细说了。
后来,我在一篇光学期刊的文章上找到了一个比较可靠的翻译,该文献中有一个名为“峰旁比”的名词,且文献内容与目标追踪相关。因此我暂且称其为峰旁比吧。个人觉得比峰值旁瓣比简洁且好听。


Why

其实中文翻译并不重要,重要的是它的作用。
在查阅文献的过程中,我看到了这样一个公式:

其中$P_{sr}^{t}$是此时第t帧的峰旁比,$f_{t}$是对于第t帧分类器预测的响应,$\mu _{t}$和$\sigma _{t}$分别是响应图$f$的均值和方差。
根据这种计算方法,我们可以大概分析一下峰旁比的作用。当初看到时,我觉得它与我在物理实验中做过的音叉共振实验中的品质因数有相似之处(品质因数$Q=\frac{f_{0}}{f_{2}-f_{1}}$)。
由公式可知,当响应中的峰值较高,且响应分布相对而言集中在峰值及周围时,峰旁比就会较高;反之,峰旁比就会较低。
那么什么时候会造成峰旁比较低呢?根据论文描述可以获得提示,当遇到遮挡,或者目标跟丢即响应区内不含目标主体时,就不会出现一个那么明显的峰值响应,同时响应也会较为分散了,此时分母较大、分子较小,峰旁比就会变低。
下面也是Visual Object Tracking using Adaptive Correlation Filters中的一张figure(这篇文章提出了MOSSE),我们需要的是a much stronger peak which translates into less drift and fewer dropped tracks。看了这个想必应该知道峰旁比发挥的作用和大致原因了。

和上面的峰值旁瓣比、峰均比相比较,显然峰旁比的定义能更好地表征响应的集中程度。


How

由峰旁比定义所得出的性质可知,峰旁比可以作为模型预测定位准确性和可信度的判据。我们可以利用峰旁比来调整Model Updater(The model updater controls the strategy and frequency of updating the observation model. It has to strike a balance between model adaptation and drift.)
我的想法是,我们可以设置一个threshold,当峰旁比小于这个threshold时,表示模型未能准确定位到目标(可信度较低),这时我们可以停止模型的更新,或者通过减小学习率等方法减慢模型的更新速度以防止模型受背景或者遮挡物较大影响,而当目标再次出现时难以复原导致最终完全跟丢的问题;而当峰旁比大于这个threshold时,我们可以实时更新模型,或者运用较大的学习率(也可以根据峰旁比将学习率划分成几个等级)。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:Peak-to-Sidelobe Ratio应用于目标跟踪

文章作者:高深远

发布时间:2020年01月18日 - 21:39

最后更新:2020年02月23日 - 21:15

原始链接:https://gsy00517.github.io/computer-vision20200118213942/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:HOG特征 | 高深远的博客

computer vision笔记:HOG特征

这几天感觉知识的海洋真的是无边无际的,一旦查资料就意味着要查更多的资料,不懂的东西简直一个接着一个。希望某天能对这些知识有个大概的把握吧,废话不多说,开始积累!

References

电子文献:
https://www.cnblogs.com/hrlnw/archive/2013/08/06/2826651.html
https://www.cnblogs.com/zyly/p/9651261.html
https://baike.baidu.com/item/HOG/9738560?fr=aladdin
https://zhuanlan.zhihu.com/p/40960756
https://www.jianshu.com/p/354acdcbae3f
https://blog.csdn.net/xjp_xujiping/article/details/89430002
https://www.leiphone.com/news/201708/ZKsGd2JRKr766wEd.html

参考文献:
[1]Histograms of Oriented Gradients for Human Detection
[2]Understanding and Diagnosing Visual Tracking Systems


特征描述子

特征描述子就是图像的一种表示,这种表示抽取了有用的信息,丢掉了不相关的信息。通常特征描述子会把一个WxHx3(3个channel)的图像转换成一个向量或者一个矩阵。


HOG特征

HOG(Histogram of Oriented Gradient),中文译为方向梯度直方图。它是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子,通过计算像局部区域中不同方向上梯度的值,然后进行累积,得到代表这块区域特征的直方图,可将其输入到分类器里面进行目标检测。
下面是论文中所呈现的进行人物检测时所采用的特征提取与目标检测的流程,本文将主要分析特征提取的部分(论文中对运用SVM分类器做检测的部分分析甚少,因此不做重点)。


思想

HOG特征的主要思想是:在图像中,局部目标的表象和形状能够被梯度或边缘的方向密度分布很好地描述。由于,梯度主要存在于边缘的地方,因此其密度分布能一定程度上描述目标物体的形状等特征。


思路

在介绍HOG特征提取的基本思路之前,我先放一张图,我认为先了解block和cell有助于理解思路,否则有点抽象,可能会(像我当初一样)看得很迷。下图的分割尺度与后文的分析一致,即一个block分成2x2的cell,一个cell分成8x8的pixel,可以先记下来。

HOG特征提取的基本思路是:

  1. 为了减少光照因素的影响,首先需要将整个图像进行归一化,这种压缩处理能够有效地降低图像局部的阴影和光照变化。
  2. 将图像分成很多小的cell,采集cell中各像素点梯度的幅值和方向,然后在每个cell中统计出一个一维的梯度方向直方图。
  3. 为了对光照和阴影有更好的不变性,我们可以在更大的范围内,对block进行对比度归一化。
  4. 在归一化后,最终获得的即为HOG描述子。

这里共做了两次normalization,不要搞混了。
那么,为什么要分成很多小的cell呢?因为用特征描述子的一个主要原因是它提供了一种紧凑的压缩表示。后面我们会看到如何把一个9个bin的直方图表示成9个数的数组。此外,对每个cell统计梯度直方图不仅可以使表示压缩而紧凑,用直方图来表示一个patch还可以使其可以更加抗噪,因为一个gradient可能会有噪音,但是用直方图来表示后就不会对噪音那么敏感了。


流程

  1. Normalize gamma & colour

    由于在图像的纹理强度中,局部的表层曝光贡献的比重比较大,因此为了减少光照因素的影响,我们首先采用Gamma校正法对输入图像的颜色空间进行归一化。
    Gamma校正可以提高图像中偏暗或者偏亮部分的对比效果,能有效降低图像局部的阴影和光照变化。其校正公式为:

    其中$I$为图像像素值,$\gamma$为校正系数。
    由幂函数的性质容易推得Gamma校正的作用,这里以灰度图像(即仅有黑白,单通道)为例做一个简单解释。

    当$\gamma$小于1时,在低灰度值区域内,动态范围变大,图像对比度增强;在高灰度值区域,动态范围变小,图像对比度降低。图像的整体灰度值变大,如下面中间的图片(左边是原图)。
    当$\gamma$大于1时,在低灰度值区域内,动态范围变小,图像对比度降低;在高灰度值区域,动态范围变大,图像对比度提高。图像的整体灰度值变小,如下面右边的图片。

    注:灰度值也称灰度等级,范围一般从0到255,白色为255,黑色为0。

    然而,是否使用Gamma校正还要视具体情况而定,当涉及大量的类内颜色变化时,比如斑马等自身就颜色变化丰富的物体,不校正效果会更好。
    上面的例子使用的是灰度图像,事实上,RGB彩色图(三通道)的performance会更好一些。可以看一下下面的对比图。

  2. Image segmentation

    分割图像这一步在论文的流程图中没有,我觉得有必要说一下,因此自行添加了一步。
    这里先说一下为什么要进行图像分割。如果对一大幅图片直接提取特征,往往得不到好的效果。因为如果提取区域比较大,那么两个完全不同的图像,也可能提取出相似的HOG特征。但这种可能性在较小的区域就很小。此外,当我们把图像分割成很多区块(patch)然后对每个区块提取特征时,这其中也包含了几何上的位置特性。例如,正面的人脸,左上部分的图像区块提取的HOG特征一般是和眼睛的HOG特征相符合的。
    HOG的图像分割策略一般有overlap和non-overlap两种,如下图所示。第一张图是overlap的情况,指的是分割出的区块会有互相重合的区域。而non-overlap指的是区块不交叠,没有重合区域。 overlap的优点在于这种分割方式可以防止对一些物体的切割,例如,如果分割的时候正好把一个眼睛从中间切割分到了两个patch中,那么提取完HOG特征之后,可能会影响接下来的分类效果。但是如果两个patch之间有一个overlap,那么这里的三个patch至少有一个内会保留完整的眼睛。overlap的缺点在于计算量大,因为重叠区域的像素需要重复计算。
    non-overlap恰恰相反,其缺点如上所述,就是有时会将一个连续的物体切割开,得到不太好的HOG特征。但它的优点是计算量小,尤其是与图像金字塔(Image Pyramid,详见computer-vision笔记:图像金字塔与高斯滤波器)相结合时,这个优点更为明显。
    在这里,我们用overlap的策略将图像分割成一个个互相重叠的block。也就是说,每个cell的直方图都会被多次用于最终的特征描述子的计算。虽然这看起来有冗余,但可以显著地提升性能。
  3. Compute gradients

    接下来就是计算图像每个像素点梯度。$G_{y}\left ( x,y \right )$算法同理。
    在具体实现时,我们可以使用如下两种kernel来实现上面梯度的计算。 接着计算梯度的幅值和方向。 可以看到,图像的梯度去掉了很多不必要的信息(比如不变的背景色),并且加重了轮廓。
  4. Weighted vote into spatial & orientation cells

    将图像划分成小的cell(矩形或者环形),然后统计每一个cell的梯度直方图,即可以得到一个cell的描述符。
    注意,这里我们在一个block内划分4个cell,即2×2个cell组成一个block,8x8个像素点组成一个cell。将一个block内每个cell的描述符串联起来即可得到一个block的HOG描述符。
    我们先来看一下各个像素点的梯度的具体计算情况。 统计梯度直方图的方式是利用刚才计算得出的cell中每一个像素点的梯度进行加权投票。我们一般考虑采用9个bin的直方图来统计一个cell中像素的梯度信息,即将cell的梯度方向0~180°(无向)或0~360°(考虑正负)分成9个bin,如下图所示: 如果cell中某一像素的梯度方向是20~40°,那么上面这个直方图第2个bin的计数就要加1,这样对cell中的每一个像素用梯度方向在直方图中进行加权投影(权值大小等于梯度幅值),将其映射到对应角度范围的bin内,就可以得到这个cell的梯度方向直方图了,即该cell对应的一个9维特征向量。若梯度方向位于相邻bin的交界处(如20°、40°等),需要对其进行方向和位置上的双线性插值。 上图是一个比较直观的加权投票的演示,这里将投影在交界处的梯度的幅值对半分至两侧的bin。
    要注意的是,如果一个角度大于160度,也就是在160至180度之间,我们知道这里角度0和180度是一样的,所以在下面这个例子里,像素的角度为165度时,就要把幅值按照比例放到0和160的bin里面去,也就是上面说的双线性插值。 事实上,我们可以采用幅值本身或者它的函数(幅值的平方根、幅值的平方、幅值的截断形式)来表示权值,但经实际测试表明:使用幅值来表示权值能获得最佳的效果。采用梯度幅值作为权重,可以使那些比较明显的边缘的方向信息对特征表达影响增大,这样比较合理,因为HOG特征主要就是依靠这些边缘纹理。
    经实验还发现,采用无向的梯度(即0~180°)和9个bin的直方图,能在行人检测试验中取得最佳的效果。
  5. Contrast normalize over overlapping spatial blocks

    由于局部光照的变化,以及前景背景对比度的变化,使得梯度强度的变化范围非常大,这就需要对梯度做局部对比度归一化。归一化能够进一步对光照、阴影、边缘进行压缩,使得特征向量对光照、阴影和边缘变化具有鲁棒性。
    我们之前已将2x2的cell组成了更大的block,现在要做的就是针对每个block进行对比度归一化。
    通常使用的HOG结构大致有三种:矩形HOG(R-HOG),圆形HOG(C-HOG)和中心环绕HOG。它们的单位都是block。实验证明,矩形HOG和圆形HOG的检测效果基本一致,而环绕形HOG效果相对差一些。 我们一般根据如下公式对block进行对比度归一化:这里$v$是该block未经归一化的特征向量,比如这里一个block含2x2个cell,每个cell对应一个9维的特征向量,那么这个block的特征向量的长度就是2x2x9。这里$\xi$是一个很小的数,主要是为了防止分母等于零而引入的。
  6. Collect HOG’s over detection window

    我们先review一下,上面我们首先把样本图片分割为若干个像素的cell,然后把梯度方向划分为9个区间,在每个cell里面对所有像素的梯度方向在各个区间进行直方图统计,得到一个9维的特征向量;然后我们使每相邻4个cell构成一个block,把一个block内的特征向量串联起来得到一个36(即2x2x9)维的特征向量。
    接下来,我们用block对样本图像进行扫描,stride为一个cell的大小,扫描完成后,我们将扫描过程中每个block的特征向量(即上述36维的)串联起来,得到样本的特征向量,也就是可以输入到后面分类器的HOG特征描述子。
    如此,就完成了HOG特征的提取。
    我们可以可视化一下最后的HOG特征,可见主要捕捉的是博尔特的躯干和腿。

总结

优点

  1. HOG特性能较好地捕捉局部形状信息,对几何和光学变化都有很好的不变性。
  2. HOG特征是在密集采样的图像块中求取的,在计算得到的HOG特征向量中隐含了该块与检测窗口之间的几何空间位置关系。

不足

  1. 很难处理遮挡问题,人体姿势动作幅度过大或物体方向改变时不易检测(这个问题后来在DPM中采用可变形部件模型的方法得到了改善)。
  2. 没有选取主方向,也没有旋转梯度方向直方图,因而本身不具有旋转不变性(较大的方向变化),其旋转不变性是通过采用不同旋转方向的训练样本来实现的。
  3. 本身不具有尺度不变性,其尺度不变性是通过缩放检测窗口图像的大小(如Image Pyramid)来实现的。
  4. 由于梯度的性质,HOG对噪点相当敏感,在实际应用中,在block和cell划分之后,对于得到各个区域,有时候还会做一次高斯平滑去除噪点。但又由于HOG特征是基于边缘的,平滑操作会降低边缘信息的对比度,从而减少图像中的有用信息,因此还需好好做个balance。

FHOG

由于后续的研究发现HOG特征可以降维。因此在HOG的基础上,又提出了FHOG(我理解为fast HOG),也取得了广泛的应用。


计算机视觉女神

或许会发现本文中好多的图例都用了同一位女士的脸,这里就扯点题外话。
照片中的女子名为Lena Soderberg,对计算机视觉领域有一定的接触的朋友应该对这张照片不会陌生。这张照片是标准的数字图像处理用例,各种算法研究经常会使用这张图作为模板。那为什么要用这幅图呢?
David C. Munson在“A Note on Lena”中给出了两条理由:

  1. 首先,该图像中各个频段的能量都很丰富,既有低频(光滑的皮肤),也有高频(帽子上的羽毛),适度地混合了细节、平滑区域、阴影和纹理,很适合来测试各种图像处理算法。
  2. 其次,Lena是位迷人的女子,能有效吸引研究者(大部分为男性)做研究。

该照片其实是一张于1972年11月出版的Playboy的中间插页。1973年,南加州大学信号图像处理研究所的副教授Alexander和学生一起,为了一个同事的学会论文正忙于寻找一幅好的图片。他们想要一幅具有良好动态范围的人的面部图片用于扫描。这时,不知是谁拿着一本Playboy走进研究室。由于当时实验室里使用的扫描仪(Muirhead wirephoto scanner)分辨率是100行每英寸,试验也仅仅需要一副512X512的图片,所以他们只将图片顶端开始的5.12英寸扫描下来,并切掉肩膀以下的部分。多年以来,由于图像Lena源于Playboy,将其引用于科技文章中饱受争议。Playboy杂志也将未授权的引用告上法庭。但随着时间的流逝,人们渐渐淡忘Lena的来源,Playboy也放松了对此的关注。值得一提的是,Lena也是Playboy发行的最畅销的海报,已经出售7,161,561份。其真正刊登的原图如下。

1997年,在图像科学和技术协会的第50届会议上,Lenna被邀为贵宾出席。在会议上,她忙于签名、拍照以及介绍自我。

说实话每次看到这张照片,想起学术界那些大牛们居然能弄出这样一个挺有意思的故事,不免觉得也挺有趣的。在我最喜欢的美剧之一《Silicon Valley》中,也有计算机视觉女神的身影。

重温大结局的时候发现也出现了。

大家对计算机视觉有兴趣的话,其实自己也打印一张摆墙上,这样就可以和懂的小伙伴们一起讨论问题了。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:HOG特征

文章作者:高深远

发布时间:2020年01月19日 - 19:51

最后更新:2020年03月15日 - 12:19

原始链接:https://gsy00517.github.io/computer-vision20200119195116/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:HOG特征 | 高深远的博客

computer vision笔记:HOG特征

这几天感觉知识的海洋真的是无边无际的,一旦查资料就意味着要查更多的资料,不懂的东西简直一个接着一个。希望某天能对这些知识有个大概的把握吧,废话不多说,开始积累!

References

电子文献:
https://www.cnblogs.com/hrlnw/archive/2013/08/06/2826651.html
https://www.cnblogs.com/zyly/p/9651261.html
https://baike.baidu.com/item/HOG/9738560?fr=aladdin
https://zhuanlan.zhihu.com/p/40960756
https://www.jianshu.com/p/354acdcbae3f
https://blog.csdn.net/xjp_xujiping/article/details/89430002
https://www.leiphone.com/news/201708/ZKsGd2JRKr766wEd.html

参考文献:
[1]Histograms of Oriented Gradients for Human Detection
[2]Understanding and Diagnosing Visual Tracking Systems


特征描述子

特征描述子就是图像的一种表示,这种表示抽取了有用的信息,丢掉了不相关的信息。通常特征描述子会把一个WxHx3(3个channel)的图像转换成一个向量或者一个矩阵。


HOG特征

HOG(Histogram of Oriented Gradient),中文译为方向梯度直方图。它是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子,通过计算像局部区域中不同方向上梯度的值,然后进行累积,得到代表这块区域特征的直方图,可将其输入到分类器里面进行目标检测。
下面是论文中所呈现的进行人物检测时所采用的特征提取与目标检测的流程,本文将主要分析特征提取的部分(论文中对运用SVM分类器做检测的部分分析甚少,因此不做重点)。


思想

HOG特征的主要思想是:在图像中,局部目标的表象和形状能够被梯度或边缘的方向密度分布很好地描述。由于,梯度主要存在于边缘的地方,因此其密度分布能一定程度上描述目标物体的形状等特征。


思路

在介绍HOG特征提取的基本思路之前,我先放一张图,我认为先了解block和cell有助于理解思路,否则有点抽象,可能会(像我当初一样)看得很迷。下图的分割尺度与后文的分析一致,即一个block分成2x2的cell,一个cell分成8x8的pixel,可以先记下来。

HOG特征提取的基本思路是:

  1. 为了减少光照因素的影响,首先需要将整个图像进行归一化,这种压缩处理能够有效地降低图像局部的阴影和光照变化。
  2. 将图像分成很多小的cell,采集cell中各像素点梯度的幅值和方向,然后在每个cell中统计出一个一维的梯度方向直方图。
  3. 为了对光照和阴影有更好的不变性,我们可以在更大的范围内,对block进行对比度归一化。
  4. 在归一化后,最终获得的即为HOG描述子。

这里共做了两次normalization,不要搞混了。
那么,为什么要分成很多小的cell呢?因为用特征描述子的一个主要原因是它提供了一种紧凑的压缩表示。后面我们会看到如何把一个9个bin的直方图表示成9个数的数组。此外,对每个cell统计梯度直方图不仅可以使表示压缩而紧凑,用直方图来表示一个patch还可以使其可以更加抗噪,因为一个gradient可能会有噪音,但是用直方图来表示后就不会对噪音那么敏感了。


流程

  1. Normalize gamma & colour

    由于在图像的纹理强度中,局部的表层曝光贡献的比重比较大,因此为了减少光照因素的影响,我们首先采用Gamma校正法对输入图像的颜色空间进行归一化。
    Gamma校正可以提高图像中偏暗或者偏亮部分的对比效果,能有效降低图像局部的阴影和光照变化。其校正公式为:

    其中$I$为图像像素值,$\gamma$为校正系数。
    由幂函数的性质容易推得Gamma校正的作用,这里以灰度图像(即仅有黑白,单通道)为例做一个简单解释。

    当$\gamma$小于1时,在低灰度值区域内,动态范围变大,图像对比度增强;在高灰度值区域,动态范围变小,图像对比度降低。图像的整体灰度值变大,如下面中间的图片(左边是原图)。
    当$\gamma$大于1时,在低灰度值区域内,动态范围变小,图像对比度降低;在高灰度值区域,动态范围变大,图像对比度提高。图像的整体灰度值变小,如下面右边的图片。

    注:灰度值也称灰度等级,范围一般从0到255,白色为255,黑色为0。

    然而,是否使用Gamma校正还要视具体情况而定,当涉及大量的类内颜色变化时,比如斑马等自身就颜色变化丰富的物体,不校正效果会更好。
    上面的例子使用的是灰度图像,事实上,RGB彩色图(三通道)的performance会更好一些。可以看一下下面的对比图。

  2. Image segmentation

    分割图像这一步在论文的流程图中没有,我觉得有必要说一下,因此自行添加了一步。
    这里先说一下为什么要进行图像分割。如果对一大幅图片直接提取特征,往往得不到好的效果。因为如果提取区域比较大,那么两个完全不同的图像,也可能提取出相似的HOG特征。但这种可能性在较小的区域就很小。此外,当我们把图像分割成很多区块(patch)然后对每个区块提取特征时,这其中也包含了几何上的位置特性。例如,正面的人脸,左上部分的图像区块提取的HOG特征一般是和眼睛的HOG特征相符合的。
    HOG的图像分割策略一般有overlap和non-overlap两种,如下图所示。第一张图是overlap的情况,指的是分割出的区块会有互相重合的区域。而non-overlap指的是区块不交叠,没有重合区域。 overlap的优点在于这种分割方式可以防止对一些物体的切割,例如,如果分割的时候正好把一个眼睛从中间切割分到了两个patch中,那么提取完HOG特征之后,可能会影响接下来的分类效果。但是如果两个patch之间有一个overlap,那么这里的三个patch至少有一个内会保留完整的眼睛。overlap的缺点在于计算量大,因为重叠区域的像素需要重复计算。
    non-overlap恰恰相反,其缺点如上所述,就是有时会将一个连续的物体切割开,得到不太好的HOG特征。但它的优点是计算量小,尤其是与图像金字塔(Image Pyramid,详见computer-vision笔记:图像金字塔与高斯滤波器)相结合时,这个优点更为明显。
    在这里,我们用overlap的策略将图像分割成一个个互相重叠的block。也就是说,每个cell的直方图都会被多次用于最终的特征描述子的计算。虽然这看起来有冗余,但可以显著地提升性能。
  3. Compute gradients

    接下来就是计算图像每个像素点梯度。$G_{y}\left ( x,y \right )$算法同理。
    在具体实现时,我们可以使用如下两种kernel来实现上面梯度的计算。 接着计算梯度的幅值和方向。 可以看到,图像的梯度去掉了很多不必要的信息(比如不变的背景色),并且加重了轮廓。
  4. Weighted vote into spatial & orientation cells

    将图像划分成小的cell(矩形或者环形),然后统计每一个cell的梯度直方图,即可以得到一个cell的描述符。
    注意,这里我们在一个block内划分4个cell,即2×2个cell组成一个block,8x8个像素点组成一个cell。将一个block内每个cell的描述符串联起来即可得到一个block的HOG描述符。
    我们先来看一下各个像素点的梯度的具体计算情况。 统计梯度直方图的方式是利用刚才计算得出的cell中每一个像素点的梯度进行加权投票。我们一般考虑采用9个bin的直方图来统计一个cell中像素的梯度信息,即将cell的梯度方向0~180°(无向)或0~360°(考虑正负)分成9个bin,如下图所示: 如果cell中某一像素的梯度方向是20~40°,那么上面这个直方图第2个bin的计数就要加1,这样对cell中的每一个像素用梯度方向在直方图中进行加权投影(权值大小等于梯度幅值),将其映射到对应角度范围的bin内,就可以得到这个cell的梯度方向直方图了,即该cell对应的一个9维特征向量。若梯度方向位于相邻bin的交界处(如20°、40°等),需要对其进行方向和位置上的双线性插值。 上图是一个比较直观的加权投票的演示,这里将投影在交界处的梯度的幅值对半分至两侧的bin。
    要注意的是,如果一个角度大于160度,也就是在160至180度之间,我们知道这里角度0和180度是一样的,所以在下面这个例子里,像素的角度为165度时,就要把幅值按照比例放到0和160的bin里面去,也就是上面说的双线性插值。 事实上,我们可以采用幅值本身或者它的函数(幅值的平方根、幅值的平方、幅值的截断形式)来表示权值,但经实际测试表明:使用幅值来表示权值能获得最佳的效果。采用梯度幅值作为权重,可以使那些比较明显的边缘的方向信息对特征表达影响增大,这样比较合理,因为HOG特征主要就是依靠这些边缘纹理。
    经实验还发现,采用无向的梯度(即0~180°)和9个bin的直方图,能在行人检测试验中取得最佳的效果。
  5. Contrast normalize over overlapping spatial blocks

    由于局部光照的变化,以及前景背景对比度的变化,使得梯度强度的变化范围非常大,这就需要对梯度做局部对比度归一化。归一化能够进一步对光照、阴影、边缘进行压缩,使得特征向量对光照、阴影和边缘变化具有鲁棒性。
    我们之前已将2x2的cell组成了更大的block,现在要做的就是针对每个block进行对比度归一化。
    通常使用的HOG结构大致有三种:矩形HOG(R-HOG),圆形HOG(C-HOG)和中心环绕HOG。它们的单位都是block。实验证明,矩形HOG和圆形HOG的检测效果基本一致,而环绕形HOG效果相对差一些。 我们一般根据如下公式对block进行对比度归一化:这里$v$是该block未经归一化的特征向量,比如这里一个block含2x2个cell,每个cell对应一个9维的特征向量,那么这个block的特征向量的长度就是2x2x9。这里$\xi$是一个很小的数,主要是为了防止分母等于零而引入的。
  6. Collect HOG’s over detection window

    我们先review一下,上面我们首先把样本图片分割为若干个像素的cell,然后把梯度方向划分为9个区间,在每个cell里面对所有像素的梯度方向在各个区间进行直方图统计,得到一个9维的特征向量;然后我们使每相邻4个cell构成一个block,把一个block内的特征向量串联起来得到一个36(即2x2x9)维的特征向量。
    接下来,我们用block对样本图像进行扫描,stride为一个cell的大小,扫描完成后,我们将扫描过程中每个block的特征向量(即上述36维的)串联起来,得到样本的特征向量,也就是可以输入到后面分类器的HOG特征描述子。
    如此,就完成了HOG特征的提取。
    我们可以可视化一下最后的HOG特征,可见主要捕捉的是博尔特的躯干和腿。

总结

优点

  1. HOG特性能较好地捕捉局部形状信息,对几何和光学变化都有很好的不变性。
  2. HOG特征是在密集采样的图像块中求取的,在计算得到的HOG特征向量中隐含了该块与检测窗口之间的几何空间位置关系。

不足

  1. 很难处理遮挡问题,人体姿势动作幅度过大或物体方向改变时不易检测(这个问题后来在DPM中采用可变形部件模型的方法得到了改善)。
  2. 没有选取主方向,也没有旋转梯度方向直方图,因而本身不具有旋转不变性(较大的方向变化),其旋转不变性是通过采用不同旋转方向的训练样本来实现的。
  3. 本身不具有尺度不变性,其尺度不变性是通过缩放检测窗口图像的大小(如Image Pyramid)来实现的。
  4. 由于梯度的性质,HOG对噪点相当敏感,在实际应用中,在block和cell划分之后,对于得到各个区域,有时候还会做一次高斯平滑去除噪点。但又由于HOG特征是基于边缘的,平滑操作会降低边缘信息的对比度,从而减少图像中的有用信息,因此还需好好做个balance。

FHOG

由于后续的研究发现HOG特征可以降维。因此在HOG的基础上,又提出了FHOG(我理解为fast HOG),也取得了广泛的应用。


计算机视觉女神

或许会发现本文中好多的图例都用了同一位女士的脸,这里就扯点题外话。
照片中的女子名为Lena Soderberg,对计算机视觉领域有一定的接触的朋友应该对这张照片不会陌生。这张照片是标准的数字图像处理用例,各种算法研究经常会使用这张图作为模板。那为什么要用这幅图呢?
David C. Munson在“A Note on Lena”中给出了两条理由:

  1. 首先,该图像中各个频段的能量都很丰富,既有低频(光滑的皮肤),也有高频(帽子上的羽毛),适度地混合了细节、平滑区域、阴影和纹理,很适合来测试各种图像处理算法。
  2. 其次,Lena是位迷人的女子,能有效吸引研究者(大部分为男性)做研究。

该照片其实是一张于1972年11月出版的Playboy的中间插页。1973年,南加州大学信号图像处理研究所的副教授Alexander和学生一起,为了一个同事的学会论文正忙于寻找一幅好的图片。他们想要一幅具有良好动态范围的人的面部图片用于扫描。这时,不知是谁拿着一本Playboy走进研究室。由于当时实验室里使用的扫描仪(Muirhead wirephoto scanner)分辨率是100行每英寸,试验也仅仅需要一副512X512的图片,所以他们只将图片顶端开始的5.12英寸扫描下来,并切掉肩膀以下的部分。多年以来,由于图像Lena源于Playboy,将其引用于科技文章中饱受争议。Playboy杂志也将未授权的引用告上法庭。但随着时间的流逝,人们渐渐淡忘Lena的来源,Playboy也放松了对此的关注。值得一提的是,Lena也是Playboy发行的最畅销的海报,已经出售7,161,561份。其真正刊登的原图如下。

1997年,在图像科学和技术协会的第50届会议上,Lenna被邀为贵宾出席。在会议上,她忙于签名、拍照以及介绍自我。

说实话每次看到这张照片,想起学术界那些大牛们居然能弄出这样一个挺有意思的故事,不免觉得也挺有趣的。在我最喜欢的美剧之一《Silicon Valley》中,也有计算机视觉女神的身影。

重温大结局的时候发现也出现了。

大家对计算机视觉有兴趣的话,其实自己也打印一张摆墙上,这样就可以和懂的小伙伴们一起讨论问题了。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:HOG特征

文章作者:高深远

发布时间:2020年01月19日 - 19:51

最后更新:2020年03月15日 - 12:19

原始链接:https://gsy00517.github.io/computer-vision20200119195116/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:图像金字塔与高斯滤波器 | 高深远的博客

computer vision笔记:图像金字塔与高斯滤波器

computer-vision笔记:HOG特征一文中,我曾提及了Image Pyramid。那么,这个图像金字塔究竟是一个什么名胜古迹呢?

References

电子文献:
https://www.cnblogs.com/ronny/p/3886013.html
https://www.cnblogs.com/wynlfd/p/9704770.html
https://www.cnblogs.com/herenzhiming/articles/5276106.html
https://www.jianshu.com/p/73e6ccbd8f3f
https://baike.baidu.com/item/%E9%AB%98%E6%96%AF%E6%BB%A4%E6%B3%A2/9032353?fr=aladdin
https://blog.csdn.net/lvquanye9483/article/details/81592574
https://zhuanlan.zhihu.com/p/94014493


原因

当用一个机器视觉系统分析未知场景时,计算机没有办法预先知识图像中物体尺度,因此,我们需要同时考虑图像在多尺度下的描述,获知感兴趣物体的最佳尺度。在很多时候,我们会将图像构建为一系列不同尺度的图像集,在不同的尺度中去检测我们感兴趣的特征。比如:在Haar特征检测人脸的时候,因为我们并不知道图像中人脸的尺寸,所以需要生成一个不同大小的图像组成的“金字塔”,扫描其中每一幅图像来寻找可能的人脸。


图像金字塔

我们可以这样得到一个图像金字塔:首先,图像经过一个低通滤波器进行平滑处理(这个步骤会使图像变模糊,类似远处的物体没有近处的清晰),然后,对这个平滑处理后的图像进行抽样(一般抽样比例在水平和竖直方向上都为1/2),从而得到一系列缩小的图像。

假设高斯金字塔的第$l$层图像为$G_{l}$,则有:

其中,$N$为高斯金字塔的层数,$R_{l}$和$C_{l}$分别为高斯金字塔第$l$层的行数和列数,$\omega \left ( m,n \right )$是一个二位可拆的5x5窗口函数,其表达式为:

上式说明,2维窗口的卷积算子,可以写成两个方向上的1维卷积核的乘积。上面卷积形式的公式实际上完成了两个功能:(1)高斯模糊;(2)降维。
按上述步骤生成的$G_{0}$,$G_{1}$,…,$G_{N}$就构成了图像的高斯金字塔,其中$G_{N}$为金字塔的底层(与原图像相同),$G_{N}$为金字塔的顶层。可见高斯金字塔的当前层图像是对其前一层图像进行高斯低通滤波、然后做隔行和隔列的降采样(去除偶数行与偶数列)生成的。其中每一层都是前一层图像大小的1/4。


高斯滤波器

本文讨论图像金字塔,怎么说着说着就变成高斯金字塔了呢。事实上,上面所提到的$\omega$其实是一个整数值的高斯核函数,进行了高斯滤波的平滑处理。

高斯滤波

这里先引入两个问题:

  1. 为什么要对图像滤波?
    主要有两个目的:(1)消除图像在数字化过程中产生或者混入的噪声;(2)提取图片对象的特征作为图像识别的特征模式。
  2. 如何理解滤波器?
    这与电路中的滤波器类似但又不同,在图像处理中,滤波器可以想象成一个包含加权系数的窗口,当使用滤波器去处理图像时,输出就相当于通过这个窗口去看这个图像。

滤波的方式有很多种,而高斯滤波是一种线性平滑滤波,适用于消除高斯噪声。
在图像处理中,高斯滤波一般有两种实现方式,一是用离散化窗口滑窗卷积,另一种通过傅里叶变换。最常见的就是第一种滑窗实现,只有当离散化的窗口非常大,用滑窗计算量非常大(尽管用了可分离滤波器依旧大)的情况下,可能会考虑基于傅里叶变化的实现方法。本文介绍的是滑窗卷积法。

高斯噪声

首先,噪声在图像中常表现为一引起较强视觉效果的孤立像素点或像素块。
而高斯噪声,就是噪声的概率密度函数服从正态分布。

高斯函数

我们首先回顾一下概率论中的高斯函数。
一维高斯分布:$G\left ( x \right )=\frac{1}{\sqrt{2\pi }\sigma }e^{-\frac{x^{2}}{2\sigma ^{2}}}$。

二维高斯分布:$G\left ( x,y \right )=\frac{1}{\sqrt{2\pi }\sigma ^{2}}e^{-\frac{x^{2}+y^{2}}{2\sigma ^{2}}}$

注:$\sigma$越大,高斯函数的高度越小,宽度越大。

如上图所示,正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。
我们只需要将“中心点”作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。这也是高斯核函数的构造原理。
理论上,高斯分布在所有定义域上都有非负值,这就需要一个无限大的卷积核。实际上,仅需要取均值周围3倍标准差内的值($3\sigma$准则),以外的部分可以直接去掉。

这里取$\sigma=1.5$,注意,由于要对这9个点计算加权平均,因此必须让它们的权重之和等于1,上图是最终所得的高斯模板。

高斯模糊

模糊处理的过程其实就是卷积的过程,使用上面的高斯模板,对图像的每一个像素进行卷积,就能使图像产生模糊平滑的效果。

上图最左边是原图9个像素点的灰度值,最右边是输出的结果。要注意的是,一次这样的操作实际上只得到了中心点的输出,即所求的加权平均结果为中心点的高斯滤波输出值。

可分离滤波器

如上面图像金字塔一节所述,2维窗口的卷积算子,可以写成两个方向上的1维卷积核的乘积。由于高斯函数可以写成可分离的形式,因此可以采用可分离滤波器实现来加速。所谓的可分离滤波器,就是可以把多维的卷积化成多个一维卷积。具体到二维的高斯滤波,就是指先对行做一维卷积,再对列做一维卷积。这样就可以将计算时间复杂度从$O\left ( M\ast M\ast N\ast N \right )$降到$O\left ( 2\ast M\ast M\ast N \right )$,这里的$M$和$N$分别是图像和滤波器的窗口大小。

性质

  1. 旋转对称性

    二维高斯函数具有旋转对称性,即滤波器在各个方向上的平滑程度是相同的。一般一幅图像的边缘方向事先是不知道的,因此,在滤波前是无法确定一个方向上比另一方向上需要更多的平滑。而旋转对称性意味着高斯平滑滤波器在后续边缘检测中不会偏向任一方向。
  2. 平滑程度易调

    高斯滤波器宽度(决定着平滑程度)是由参数$\sigma$表征的,而且$\sigma$和平滑程度的关系是非常简单的。$\sigma$越大,高斯滤波器的频带就越宽,平滑程度就越好。通过调节平滑程度参数$\sigma$,可在图像特征过分模糊(过平滑)与平滑图像中由于噪声和细纹理所引起的过多的不希望突变量(欠平滑)之间取得balance。
    此外,高斯函数是单值函数。这表明,高斯滤波器用像素邻域的加权均值来代替该点的像素值,而每一邻域像素点权值是随该点与中心点的距离单调增减的。这一性质很重要,因为边缘是一种图像局部特征,如果平滑运算对离算子中心很远的像素点仍然有很大作用,则平滑运算会使图像失真。
  3. 平滑可以层叠

    由性质:两个高斯核的卷积等同于另外一个不同核参数的高斯核卷积。可以推得:不同的高斯核对图像的平滑是连续的。
  4. 可分离性

    根据上文分析,由于高斯函数的可分离性,大高斯滤波器可以高效地实现。二维高斯函数卷积可以分两步来进行,首先将图像与一维高斯核进行卷积,然后将卷积结果与方向垂直且函数形式相同一维高斯核再进行卷积。如此,二维高斯滤波的计算量随滤波模板宽度$N$成线性增长而不是成平方增长。

值得一提的是,在Young对生理学的研究中发现,哺乳动物的视网膜和视觉皮层的感受区域可以很好地用4阶以内的高斯微分来建模。


拉普拉斯金字塔

由于高斯金字塔用于图片下采样(即减小图片的尺寸),是从金字塔的底层到上层自下而上的。而高斯滤波构造的图像金字塔具有局部极值递性,即图像的特征是在减少的。
那么我们自然而然会想到,需不需要一个自上而下的金字塔用于上采样,和高斯金字塔配合使用。
这里就引入了拉普拉斯金字塔,它可以认为是一个残差金字塔,用来存储下采样后图片与原始图片的差异。其每一层的图像为同一层高斯金字塔的图像减去上一层的图像进行上采样并高斯模糊的结果。

注意,高斯金字塔的下采样是不可逆的,可以这样理解:下采样过程丢失的信息不能通过上采样来完全恢复,即高斯金字塔中任意一张图$G_{i}$先进行下采样得到图$Down(G_{i})$,再进行上采样得到图$Up(Down(G_{i}))$,此时的$Up(Down(G_{i}))$与原本的$G_{i}$是存在差异的。而拉普拉斯金字塔的作用,就是记录高斯金字塔每一层下采样后再上采样得到的结果与下采样前的原图之间差异,其目的是为了能够完整的恢复出每一层的下采样前图像。可以用下面这个公式来简单表述:

若将第$i+1$层的高斯金字塔从顶层开始依次加上第$i$层拉普拉斯金子塔,那么就几乎可以复原原来图像(有点绕,建议结合上图体会)。
拉普拉斯的具体构造过程如下:

  1. 内插

    将$G_{l}$进行内插(这里是用与降维时相同的滤波核而不是双线性插值),得到放大的图像$G_{l}^{\ast }$,使$G_{l}^{\ast }$的尺寸与$G_{l-1}$的尺寸相同,表示为:这边的参数就不说明了,与上文相同。需要注意的是,这里的系数取$4$,是因为每次能参与加权的项的权值之和为4/256,这与$\omega$的选取有关。
  2. 相减

    原理上文已经说了,接下来我们自上而下构造拉普拉斯金字塔。

如下图所示,此为文章前面小猫的图像金字塔所生成的拉普拉斯金字塔。不要以为这张图搞错了,细看的话就会发现,除了顶层的图片之外,下面的图片(大尺寸的)中仅有一些零散的点(不是屏幕上的灰尘)和淡淡的线,也就是残差。可以通过Gamma校正使这些残差特征更加清晰。

关于Gamma校正,可以看一看上一篇文章computer-vision笔记:HOG特征,关于图像金字塔和高斯滤波器就说到这里了。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:图像金字塔与高斯滤波器

文章作者:高深远

发布时间:2020年01月19日 - 23:10

最后更新:2020年01月28日 - 09:47

原始链接:https://gsy00517.github.io/computer-vision20200119231033/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:图像金字塔与高斯滤波器 | 高深远的博客

computer vision笔记:图像金字塔与高斯滤波器

computer-vision笔记:HOG特征一文中,我曾提及了Image Pyramid。那么,这个图像金字塔究竟是一个什么名胜古迹呢?

References

电子文献:
https://www.cnblogs.com/ronny/p/3886013.html
https://www.cnblogs.com/wynlfd/p/9704770.html
https://www.cnblogs.com/herenzhiming/articles/5276106.html
https://www.jianshu.com/p/73e6ccbd8f3f
https://baike.baidu.com/item/%E9%AB%98%E6%96%AF%E6%BB%A4%E6%B3%A2/9032353?fr=aladdin
https://blog.csdn.net/lvquanye9483/article/details/81592574
https://zhuanlan.zhihu.com/p/94014493


原因

当用一个机器视觉系统分析未知场景时,计算机没有办法预先知识图像中物体尺度,因此,我们需要同时考虑图像在多尺度下的描述,获知感兴趣物体的最佳尺度。在很多时候,我们会将图像构建为一系列不同尺度的图像集,在不同的尺度中去检测我们感兴趣的特征。比如:在Haar特征检测人脸的时候,因为我们并不知道图像中人脸的尺寸,所以需要生成一个不同大小的图像组成的“金字塔”,扫描其中每一幅图像来寻找可能的人脸。


图像金字塔

我们可以这样得到一个图像金字塔:首先,图像经过一个低通滤波器进行平滑处理(这个步骤会使图像变模糊,类似远处的物体没有近处的清晰),然后,对这个平滑处理后的图像进行抽样(一般抽样比例在水平和竖直方向上都为1/2),从而得到一系列缩小的图像。

假设高斯金字塔的第$l$层图像为$G_{l}$,则有:

其中,$N$为高斯金字塔的层数,$R_{l}$和$C_{l}$分别为高斯金字塔第$l$层的行数和列数,$\omega \left ( m,n \right )$是一个二位可拆的5x5窗口函数,其表达式为:

上式说明,2维窗口的卷积算子,可以写成两个方向上的1维卷积核的乘积。上面卷积形式的公式实际上完成了两个功能:(1)高斯模糊;(2)降维。
按上述步骤生成的$G_{0}$,$G_{1}$,…,$G_{N}$就构成了图像的高斯金字塔,其中$G_{N}$为金字塔的底层(与原图像相同),$G_{N}$为金字塔的顶层。可见高斯金字塔的当前层图像是对其前一层图像进行高斯低通滤波、然后做隔行和隔列的降采样(去除偶数行与偶数列)生成的。其中每一层都是前一层图像大小的1/4。


高斯滤波器

本文讨论图像金字塔,怎么说着说着就变成高斯金字塔了呢。事实上,上面所提到的$\omega$其实是一个整数值的高斯核函数,进行了高斯滤波的平滑处理。

高斯滤波

这里先引入两个问题:

  1. 为什么要对图像滤波?
    主要有两个目的:(1)消除图像在数字化过程中产生或者混入的噪声;(2)提取图片对象的特征作为图像识别的特征模式。
  2. 如何理解滤波器?
    这与电路中的滤波器类似但又不同,在图像处理中,滤波器可以想象成一个包含加权系数的窗口,当使用滤波器去处理图像时,输出就相当于通过这个窗口去看这个图像。

滤波的方式有很多种,而高斯滤波是一种线性平滑滤波,适用于消除高斯噪声。
在图像处理中,高斯滤波一般有两种实现方式,一是用离散化窗口滑窗卷积,另一种通过傅里叶变换。最常见的就是第一种滑窗实现,只有当离散化的窗口非常大,用滑窗计算量非常大(尽管用了可分离滤波器依旧大)的情况下,可能会考虑基于傅里叶变化的实现方法。本文介绍的是滑窗卷积法。

高斯噪声

首先,噪声在图像中常表现为一引起较强视觉效果的孤立像素点或像素块。
而高斯噪声,就是噪声的概率密度函数服从正态分布。

高斯函数

我们首先回顾一下概率论中的高斯函数。
一维高斯分布:$G\left ( x \right )=\frac{1}{\sqrt{2\pi }\sigma }e^{-\frac{x^{2}}{2\sigma ^{2}}}$。

二维高斯分布:$G\left ( x,y \right )=\frac{1}{\sqrt{2\pi }\sigma ^{2}}e^{-\frac{x^{2}+y^{2}}{2\sigma ^{2}}}$

注:$\sigma$越大,高斯函数的高度越小,宽度越大。

如上图所示,正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。
我们只需要将“中心点”作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。这也是高斯核函数的构造原理。
理论上,高斯分布在所有定义域上都有非负值,这就需要一个无限大的卷积核。实际上,仅需要取均值周围3倍标准差内的值($3\sigma$准则),以外的部分可以直接去掉。

这里取$\sigma=1.5$,注意,由于要对这9个点计算加权平均,因此必须让它们的权重之和等于1,上图是最终所得的高斯模板。

高斯模糊

模糊处理的过程其实就是卷积的过程,使用上面的高斯模板,对图像的每一个像素进行卷积,就能使图像产生模糊平滑的效果。

上图最左边是原图9个像素点的灰度值,最右边是输出的结果。要注意的是,一次这样的操作实际上只得到了中心点的输出,即所求的加权平均结果为中心点的高斯滤波输出值。

可分离滤波器

如上面图像金字塔一节所述,2维窗口的卷积算子,可以写成两个方向上的1维卷积核的乘积。由于高斯函数可以写成可分离的形式,因此可以采用可分离滤波器实现来加速。所谓的可分离滤波器,就是可以把多维的卷积化成多个一维卷积。具体到二维的高斯滤波,就是指先对行做一维卷积,再对列做一维卷积。这样就可以将计算时间复杂度从$O\left ( M\ast M\ast N\ast N \right )$降到$O\left ( 2\ast M\ast M\ast N \right )$,这里的$M$和$N$分别是图像和滤波器的窗口大小。

性质

  1. 旋转对称性

    二维高斯函数具有旋转对称性,即滤波器在各个方向上的平滑程度是相同的。一般一幅图像的边缘方向事先是不知道的,因此,在滤波前是无法确定一个方向上比另一方向上需要更多的平滑。而旋转对称性意味着高斯平滑滤波器在后续边缘检测中不会偏向任一方向。
  2. 平滑程度易调

    高斯滤波器宽度(决定着平滑程度)是由参数$\sigma$表征的,而且$\sigma$和平滑程度的关系是非常简单的。$\sigma$越大,高斯滤波器的频带就越宽,平滑程度就越好。通过调节平滑程度参数$\sigma$,可在图像特征过分模糊(过平滑)与平滑图像中由于噪声和细纹理所引起的过多的不希望突变量(欠平滑)之间取得balance。
    此外,高斯函数是单值函数。这表明,高斯滤波器用像素邻域的加权均值来代替该点的像素值,而每一邻域像素点权值是随该点与中心点的距离单调增减的。这一性质很重要,因为边缘是一种图像局部特征,如果平滑运算对离算子中心很远的像素点仍然有很大作用,则平滑运算会使图像失真。
  3. 平滑可以层叠

    由性质:两个高斯核的卷积等同于另外一个不同核参数的高斯核卷积。可以推得:不同的高斯核对图像的平滑是连续的。
  4. 可分离性

    根据上文分析,由于高斯函数的可分离性,大高斯滤波器可以高效地实现。二维高斯函数卷积可以分两步来进行,首先将图像与一维高斯核进行卷积,然后将卷积结果与方向垂直且函数形式相同一维高斯核再进行卷积。如此,二维高斯滤波的计算量随滤波模板宽度$N$成线性增长而不是成平方增长。

值得一提的是,在Young对生理学的研究中发现,哺乳动物的视网膜和视觉皮层的感受区域可以很好地用4阶以内的高斯微分来建模。


拉普拉斯金字塔

由于高斯金字塔用于图片下采样(即减小图片的尺寸),是从金字塔的底层到上层自下而上的。而高斯滤波构造的图像金字塔具有局部极值递性,即图像的特征是在减少的。
那么我们自然而然会想到,需不需要一个自上而下的金字塔用于上采样,和高斯金字塔配合使用。
这里就引入了拉普拉斯金字塔,它可以认为是一个残差金字塔,用来存储下采样后图片与原始图片的差异。其每一层的图像为同一层高斯金字塔的图像减去上一层的图像进行上采样并高斯模糊的结果。

注意,高斯金字塔的下采样是不可逆的,可以这样理解:下采样过程丢失的信息不能通过上采样来完全恢复,即高斯金字塔中任意一张图$G_{i}$先进行下采样得到图$Down(G_{i})$,再进行上采样得到图$Up(Down(G_{i}))$,此时的$Up(Down(G_{i}))$与原本的$G_{i}$是存在差异的。而拉普拉斯金字塔的作用,就是记录高斯金字塔每一层下采样后再上采样得到的结果与下采样前的原图之间差异,其目的是为了能够完整的恢复出每一层的下采样前图像。可以用下面这个公式来简单表述:

若将第$i+1$层的高斯金字塔从顶层开始依次加上第$i$层拉普拉斯金子塔,那么就几乎可以复原原来图像(有点绕,建议结合上图体会)。
拉普拉斯的具体构造过程如下:

  1. 内插

    将$G_{l}$进行内插(这里是用与降维时相同的滤波核而不是双线性插值),得到放大的图像$G_{l}^{\ast }$,使$G_{l}^{\ast }$的尺寸与$G_{l-1}$的尺寸相同,表示为:这边的参数就不说明了,与上文相同。需要注意的是,这里的系数取$4$,是因为每次能参与加权的项的权值之和为4/256,这与$\omega$的选取有关。
  2. 相减

    原理上文已经说了,接下来我们自上而下构造拉普拉斯金字塔。

如下图所示,此为文章前面小猫的图像金字塔所生成的拉普拉斯金字塔。不要以为这张图搞错了,细看的话就会发现,除了顶层的图片之外,下面的图片(大尺寸的)中仅有一些零散的点(不是屏幕上的灰尘)和淡淡的线,也就是残差。可以通过Gamma校正使这些残差特征更加清晰。

关于Gamma校正,可以看一看上一篇文章computer-vision笔记:HOG特征,关于图像金字塔和高斯滤波器就说到这里了。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:图像金字塔与高斯滤波器

文章作者:高深远

发布时间:2020年01月19日 - 23:10

最后更新:2020年01月28日 - 09:47

原始链接:https://gsy00517.github.io/computer-vision20200119231033/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:相关滤波与KCF | 高深远的博客

computer vision笔记:相关滤波与KCF

相关滤波(cross-correlation)是在目标跟踪领域一种非常强大的方法,主打简洁和高速,各种基于相关滤波的算法层出不穷。其中,KCF(不是肯德基)是一个非常经典的算法,在目标跟踪领域虽说它不是最早运用相关滤波的算法(MOSSE要早于它),但是它对之后运用相关滤波进行目标跟踪的这一系列算法有重要的奠基作用。本文就最近对这些方面的了解,结合自己的思考,做一个简单的整理归纳,如有疏漏之处还请多多指教。

References

电子文献:
https://blog.csdn.net/fhcfhc1112/article/details/83783588
https://blog.csdn.net/weixin_39467358/article/details/83304082
https://www.cnblogs.com/jins-note/p/10215511.html
https://blog.csdn.net/li_dongxuan/article/details/70667137?locationNum=5&fps=1
https://www.leiphone.com/news/201709/kLil97MnXF8Gh3sC.html

参考文献:
[1]High-Speed Tracking with Kernelized Correlation Filters
[2]Visual Object Tracking using Adaptive Correlation Filters
[3]Exploiting the Circulant Structure of Tracking-by-detection with Kernels


建议

由于KCF这篇文章主要是从理论上面来论述相关滤波来做tracking,其中涉及数学、理论的东西还是挺繁琐的。因此在正文开始之前,推荐可以先看一下我之前总结的几篇有关的博文,后文涉及到的话就不详细写了。
循环矩阵:linear-algebra笔记:循环矩阵
HOG特征:computer-vision笔记:HOG特征
正负样本:machine-learning笔记:数据采样之正样本和负样本
闭式解:machine-learning笔记:闭式解
此外,还可以先看看b站up主桥本环关于相关滤波两个算法的讲解视频目标跟踪:相关滤波算法MOSSE实现代码讲解目标跟踪:相关滤波算法KCF实现代码讲解,个人觉得讲得挺不错的。


论文

这里分别是MOSSEKCF两个算法的论文,我在阅读时已用黄色高亮一部分重点。同时本篇文章也主要参考了这两篇paper,可以先看,也可以看完本文再看。


相关滤波

先来看一个公式:

这里$:=$表示“定义为”(等效于等号上加个delta或者def),$\ast$表示复数共轭(complex-conjugate),而这里的$\star$表示的就是相关滤波操作。
你可能会觉得这个式子非常熟悉,是的,它非常像卷积的式子。但是不同的是,一般的卷积操作为$\int_{-\infty }^{\infty }f\left ( t \right )g\left ( t-\tau \right )dt$,而在相关滤波这里是加号。这就是说,在卷积的时候,我们需要把模板先进行翻转,再进行卷积,而相关操作就不需要了。
其实相关操作就是用来衡量两个信号是否相关,当两个信号越相似、相关性越强的时候,他们做相关操作输出的响应就会越强。用到目标跟踪里面,当做相关操作的两个框中的目标越相似,我们就会获得越高的响应。
相关滤波的实际意义是把输入图像映射到一个理想响应图,将这个响应图当中的最高峰与目标中心点对应起来,也就是我们预测的目标接下来的位置。它一个最主要的优点就是能够借助于傅里叶变换,从而快速计算大量的候选样本的响应值。


循环矩阵

在论文High-Speed Tracking with Kernelized Correlation Filters的introduction部分,有这样一句话:“we argue that undersampling negatives is the main factor inhibiting performance in tracking.”也就是说,负样本的欠采样是阻碍跟踪效果的主要因素。这在之前的文章中介绍过,这里就不在细述了。
这句话主要针对的问题是我们可以从一张图像中获得几乎无限的负样本。但由于跟踪的时间敏感性,我们的跟踪算法只能在尽可能多地采集样本和维持较低的计算需求之间取得一个平衡。之前通常的做法是从每一帧中随机选择几个样本。
KCF的一大贡献就是采用了一种更方便的方法迅速获取更多的负样本,以便于能够训练出一个更好的分类器。作者发现,在傅里叶域中,如果我们使用特定的模型进行转换,一些学习算法实际上变得更容易(in the Fourier domain, some learning algorithms actually become easier as we add more samples, if we use a specific model for translations)。
具体的做法如下,首先利用一个n维列向量来表示目标,记为$x$,然后利用$x$和一个循环移位矩阵$P$生成一个循环矩阵,其目的是使用一个base sample(正样本)和生成多个虚拟样本(负样本)来训练一个分类器。


根据循环特性可以推出下面两点:

  1. 可以周期性的获得同样的信号。
  2. 同样的,我们可以把上面的变换等效成将base sample即生成向量正向移动一半长度和反向移动一半长度组合而成。

这里是一个循环图片的示例,使用base sample,若我们向下移动15个像素,也就是从下面剪切15个像素拼到上面,就会变成左二图,若移动30个就可以生成左一图,右侧的图片是上移生成的。这就是在做tracking时循环采样的样本,一般会在目标周围取一个比目标更大的一个框,然后对大框框取的图像区域进行循环采样,那么就会生成这样一些新的样本来模拟我们的正样本并用于训练。

注意:这些新样本称为虚拟样本,不属于正样本但可以用于回归训练。

获得了这样一个循环矩阵之后,作者接下来说:“all circulant matrices are made diagonal by the Discrete Fourier Transform(DFT), regardless of the generating vector x.”就是说循环矩阵的生成向量是完全指定的,且循环矩阵有一个非常好的性质:对任意生成向量$\widehat{x}$,我们都可以通过离散傅立叶变换(具有线性性)对循环矩阵进行对角化表示。

这里的$F$是一个与向量$\widehat{x}$无关的常数矩阵,如果这里看得不懂的话,可以参照linear-algebra笔记:循环矩阵
如此,在傅里叶域内,用离散傅里叶变换来做之后的计算,对速度会有非常大的提升。
用到循环矩阵后,有两个常用的公式,可以参考我之前的文章。


训练

KCF做训练时所用的是岭回归。在线性情况下,岭回归的优化目标方程如下所示:

其闭式解为:

此时就可以利用循环矩阵在傅里叶域计算的性质来求解了。最后求出如下式子:

从原来的矩阵相乘和求逆,转换到傅里叶域的点乘和点除(这里的除号是点除),一下子运算就简单了许多。
在非线形的情况下,也可以得到一个同样的情况,这里需要引入一个满足条件的核,例如高斯核、线性核等,最后可计算得出一个闭式解。

非线性情况下,引入核可得到一个类似的岭回归的优化目标方程:

这里我们定义核函数$\kappa$为基向量$\varphi \left ( x \right )$之间的点积,即$\varphi^{T} \left ( x \right )\varphi \left ( x{}’ \right )=\kappa \left ( x,x{}’ \right )$。
在岭回归/脊回归(Ridge Regression)中,闭式解的基本形式如下:

这里$K$表示核空间的核矩阵,由核函数得到$K_{ij}=\kappa \left ( x_{i},x_{j} \right )=\varphi \left ( X \right )\varphi \left ( X \right )^{T}$。
最终可得:

这里的除号也是点除,此时求出来的$K$和$\alpha$就可以来做tracking了。同样的,我们还是利用循环矩阵的性质并且在傅里叶域内来做计算。


快速检测

我们很少希望单独来评估一个图像块的回归函数$f\left ( z \right )$。为了检测感兴趣的目标对象,我们通常希望在几个图像位置上评估$f\left ( z \right )$,这几个候选块(candidate patches)可以通过循环移位来建模。
定义$K^{z}$表示所有训练样本和所有候选块之间的核矩阵$K^{z}=\varphi \left ( X \right )\varphi \left ( Z \right )^{T}$。由于样本和图像块都是分别通过基础样本$x$和基础图像块$z$的循环移位组成的,因此矩阵$K^{z}$的每个元素可以表示为:$K_{i,j}=k(P^{i-1}z,P^{j-1}x)$。这里的$P$表示的是位移矩阵。易验证,$K^{z}$也是循环矩阵。
可计算得到各测试样本的响应值:

最后,我们可以求得一张feature map,也就是一张二维的响应图。


附录

这里补充在原论文的appendix中提到的两个问题。

  1. 余弦窗

    如果不加余弦窗,除了那个最原始样本,其他循环移位生成的样本的边缘都比较突兀,也就说这些样本数据是比较差的,会干扰训练的结果。而如果加了余弦窗。 上面是没加余弦窗的情况。除了那个最原始样本,其他样本都是合成的,如果不加余弦窗,那么这些合成样本就会降低分类器的判别能力(如上面c图所示,16张样本中只有两张比较合理)。如果加了余弦窗,就使得图像边缘像素值就都接近于或者等于0,因此循环移位过程中只要目标保持完整那这个样本就是合理的,只有当目标中心接近边缘时,目标跨越边界的那些样本才是错误的,这样以来虽不真实但合理的样本数量就可以增加到大约2/3。
  2. regression target y

    这个y是高斯加权后的值。初始目标的位置在padding后的search window的中心,循环移位得到的多个样本反应的是背景信息,而且离中心越远,就越不是目标,所以我们对标签进行高斯加权就刚好可以体现这种可能性准则。KCF里的输出是一个二维response矩阵,里面元素的大小代表该位置下的目标为预测目标的可能性,因此,在训练的时候就是输入是特征,而输出是一个gaussian_shaped_label,一般分类的标签是二值的,或者多值离散的,但是这个高斯标签反应的是由初始目标移位采样形成的若干样本距离初识样本越近可能性越大的准则,在代码中,高斯的峰值被移动到了左上角(于是四个角的值偏大),原因在论文的附录中进行了解释:“after computing a cross-correlation between two images in the Fourier domain and converting back to the spatial domain, it is the top-left element of the result that corresponds to a shift of zero”,也就是说目标零位移对应的是左上角的值。这样一来,我们在预测目标位置的时候,只需要pos=pos+find(response==max(response(:)))就好。如果把峰值放在中心点的话,就会“unnecessarily cause the detection output to be shifted by half a window”。

    补充:在代码中对目标进行padding是为了能让样本中含有特别需要学习的背景信息,而且可以尽量保证样本中目标的完整性,这是考虑循环移位将目标打散了。另外有关高斯加权,可以看一下我的文章computer-vision笔记:图像金字塔与高斯滤波器


效果

这是KCF在OTB2013上面做的一个实验,由于当时效果比较好的是struck(所以逃不了被针对的命运)。可以看到,KCF(使用HOG特征+高斯核函数)和DCF(也是同一个作者同一篇论文提出的,使用HOG特征+线性核函数,称为对偶相关滤波器)相比于struck来说,精度取得了显著的提升,从0.656提升到了0.732/0.728。

我们还可以根据这个统计图来看一下速度,即使使用了HOG特征和高斯核,KCF的速度还能达到172帧每秒。
此外,用了多通道扩展的DCF取得了更快的速度,但就精度而言较KCF稍差,但也是质的飞跃了。
相较而言,即使用了非常朴素的raw pixels,尽管效果比HOG特征差好多,但是速度并没有提高。这里也证明了HOG特征的强大。
另外可以看到KCF的祖宗MOSSE速度非常亮眼,但这是因为MOSSE它只用了简单的灰度特征,而不是HOG这样高维的特征,可想而知精确度总体效果还是要差一大截的。


缺点

  1. 缺点一

    对尺度变化的适应性不强。解决办法是加一个尺度变化的比例系数进行多次检测,代价是牺牲一些速度。
  2. 缺点二

    对目标快速变形(假设用的是HOG特征)或颜色快速变化(假设用的是颜色特征)不鲁棒。毕竟相关滤波是一种模板类的方法。HOG描述的就是形状信息,形状变化得太快必然会导致效果变差。而如果快速变色,那基于颜色特征的模板肯定也就跟不上了。这个还和模型更新策略与更新速度有关。若采用固定学习率的线性加权更新,那么如果学习率太大,部分或短暂遮挡和任何检测不准确,模型就会学习到背景信息,积累到一定程度模型被背景带飞了;如果学习率太小,目标已经变形了而模板还是那个模板,就会不认识目标,也会降低效果。
  3. 缺点三

    对物体快速运动或者低帧率视频不太鲁棒。这两种情况都是意味着在跟踪过程中下一帧图像中目标的位置偏离search window中心太远(要么靠近边缘,要么出去一半,要么全出去)。由于我们是给样本加了余弦窗的,也就是说目标位置靠近边缘会由于余弦窗的存在损失了部分目标信息(变成0),更不用说那些目标超出search window一半或者全超出去的情况了,这也就是CF类算法中的边界效应(Boundary Effects)。

其他

头一回看这么“理论”的论文我真的头都大了,要全部搞懂的话估计要花整整一天还不够。真的不得不佩服科研工作者们的智慧,我还是老老实实打基础吧。

原文的理论性、数学性更强,本文把主要的几个核心公式整理了一下,有些许修改和添加,如有疏漏还请多多指教。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:相关滤波与KCF

文章作者:高深远

发布时间:2020年01月20日 - 12:08

最后更新:2020年03月27日 - 23:19

原始链接:https://gsy00517.github.io/computer-vision20200120120823/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:相关滤波与KCF | 高深远的博客

computer vision笔记:相关滤波与KCF

相关滤波(cross-correlation)是在目标跟踪领域一种非常强大的方法,主打简洁和高速,各种基于相关滤波的算法层出不穷。其中,KCF(不是肯德基)是一个非常经典的算法,在目标跟踪领域虽说它不是最早运用相关滤波的算法(MOSSE要早于它),但是它对之后运用相关滤波进行目标跟踪的这一系列算法有重要的奠基作用。本文就最近对这些方面的了解,结合自己的思考,做一个简单的整理归纳,如有疏漏之处还请多多指教。

References

电子文献:
https://blog.csdn.net/fhcfhc1112/article/details/83783588
https://blog.csdn.net/weixin_39467358/article/details/83304082
https://www.cnblogs.com/jins-note/p/10215511.html
https://blog.csdn.net/li_dongxuan/article/details/70667137?locationNum=5&fps=1
https://www.leiphone.com/news/201709/kLil97MnXF8Gh3sC.html

参考文献:
[1]High-Speed Tracking with Kernelized Correlation Filters
[2]Visual Object Tracking using Adaptive Correlation Filters
[3]Exploiting the Circulant Structure of Tracking-by-detection with Kernels


建议

由于KCF这篇文章主要是从理论上面来论述相关滤波来做tracking,其中涉及数学、理论的东西还是挺繁琐的。因此在正文开始之前,推荐可以先看一下我之前总结的几篇有关的博文,后文涉及到的话就不详细写了。
循环矩阵:linear-algebra笔记:循环矩阵
HOG特征:computer-vision笔记:HOG特征
正负样本:machine-learning笔记:数据采样之正样本和负样本
闭式解:machine-learning笔记:闭式解
此外,还可以先看看b站up主桥本环关于相关滤波两个算法的讲解视频目标跟踪:相关滤波算法MOSSE实现代码讲解目标跟踪:相关滤波算法KCF实现代码讲解,个人觉得讲得挺不错的。


论文

这里分别是MOSSEKCF两个算法的论文,我在阅读时已用黄色高亮一部分重点。同时本篇文章也主要参考了这两篇paper,可以先看,也可以看完本文再看。


相关滤波

先来看一个公式:

这里$:=$表示“定义为”(等效于等号上加个delta或者def),$\ast$表示复数共轭(complex-conjugate),而这里的$\star$表示的就是相关滤波操作。
你可能会觉得这个式子非常熟悉,是的,它非常像卷积的式子。但是不同的是,一般的卷积操作为$\int_{-\infty }^{\infty }f\left ( t \right )g\left ( t-\tau \right )dt$,而在相关滤波这里是加号。这就是说,在卷积的时候,我们需要把模板先进行翻转,再进行卷积,而相关操作就不需要了。
其实相关操作就是用来衡量两个信号是否相关,当两个信号越相似、相关性越强的时候,他们做相关操作输出的响应就会越强。用到目标跟踪里面,当做相关操作的两个框中的目标越相似,我们就会获得越高的响应。
相关滤波的实际意义是把输入图像映射到一个理想响应图,将这个响应图当中的最高峰与目标中心点对应起来,也就是我们预测的目标接下来的位置。它一个最主要的优点就是能够借助于傅里叶变换,从而快速计算大量的候选样本的响应值。


循环矩阵

在论文High-Speed Tracking with Kernelized Correlation Filters的introduction部分,有这样一句话:“we argue that undersampling negatives is the main factor inhibiting performance in tracking.”也就是说,负样本的欠采样是阻碍跟踪效果的主要因素。这在之前的文章中介绍过,这里就不在细述了。
这句话主要针对的问题是我们可以从一张图像中获得几乎无限的负样本。但由于跟踪的时间敏感性,我们的跟踪算法只能在尽可能多地采集样本和维持较低的计算需求之间取得一个平衡。之前通常的做法是从每一帧中随机选择几个样本。
KCF的一大贡献就是采用了一种更方便的方法迅速获取更多的负样本,以便于能够训练出一个更好的分类器。作者发现,在傅里叶域中,如果我们使用特定的模型进行转换,一些学习算法实际上变得更容易(in the Fourier domain, some learning algorithms actually become easier as we add more samples, if we use a specific model for translations)。
具体的做法如下,首先利用一个n维列向量来表示目标,记为$x$,然后利用$x$和一个循环移位矩阵$P$生成一个循环矩阵,其目的是使用一个base sample(正样本)和生成多个虚拟样本(负样本)来训练一个分类器。


根据循环特性可以推出下面两点:

  1. 可以周期性的获得同样的信号。
  2. 同样的,我们可以把上面的变换等效成将base sample即生成向量正向移动一半长度和反向移动一半长度组合而成。

这里是一个循环图片的示例,使用base sample,若我们向下移动15个像素,也就是从下面剪切15个像素拼到上面,就会变成左二图,若移动30个就可以生成左一图,右侧的图片是上移生成的。这就是在做tracking时循环采样的样本,一般会在目标周围取一个比目标更大的一个框,然后对大框框取的图像区域进行循环采样,那么就会生成这样一些新的样本来模拟我们的正样本并用于训练。

注意:这些新样本称为虚拟样本,不属于正样本但可以用于回归训练。

获得了这样一个循环矩阵之后,作者接下来说:“all circulant matrices are made diagonal by the Discrete Fourier Transform(DFT), regardless of the generating vector x.”就是说循环矩阵的生成向量是完全指定的,且循环矩阵有一个非常好的性质:对任意生成向量$\widehat{x}$,我们都可以通过离散傅立叶变换(具有线性性)对循环矩阵进行对角化表示。

这里的$F$是一个与向量$\widehat{x}$无关的常数矩阵,如果这里看得不懂的话,可以参照linear-algebra笔记:循环矩阵
如此,在傅里叶域内,用离散傅里叶变换来做之后的计算,对速度会有非常大的提升。
用到循环矩阵后,有两个常用的公式,可以参考我之前的文章。


训练

KCF做训练时所用的是岭回归。在线性情况下,岭回归的优化目标方程如下所示:

其闭式解为:

此时就可以利用循环矩阵在傅里叶域计算的性质来求解了。最后求出如下式子:

从原来的矩阵相乘和求逆,转换到傅里叶域的点乘和点除(这里的除号是点除),一下子运算就简单了许多。
在非线形的情况下,也可以得到一个同样的情况,这里需要引入一个满足条件的核,例如高斯核、线性核等,最后可计算得出一个闭式解。

非线性情况下,引入核可得到一个类似的岭回归的优化目标方程:

这里我们定义核函数$\kappa$为基向量$\varphi \left ( x \right )$之间的点积,即$\varphi^{T} \left ( x \right )\varphi \left ( x{}’ \right )=\kappa \left ( x,x{}’ \right )$。
在岭回归/脊回归(Ridge Regression)中,闭式解的基本形式如下:

这里$K$表示核空间的核矩阵,由核函数得到$K_{ij}=\kappa \left ( x_{i},x_{j} \right )=\varphi \left ( X \right )\varphi \left ( X \right )^{T}$。
最终可得:

这里的除号也是点除,此时求出来的$K$和$\alpha$就可以来做tracking了。同样的,我们还是利用循环矩阵的性质并且在傅里叶域内来做计算。


快速检测

我们很少希望单独来评估一个图像块的回归函数$f\left ( z \right )$。为了检测感兴趣的目标对象,我们通常希望在几个图像位置上评估$f\left ( z \right )$,这几个候选块(candidate patches)可以通过循环移位来建模。
定义$K^{z}$表示所有训练样本和所有候选块之间的核矩阵$K^{z}=\varphi \left ( X \right )\varphi \left ( Z \right )^{T}$。由于样本和图像块都是分别通过基础样本$x$和基础图像块$z$的循环移位组成的,因此矩阵$K^{z}$的每个元素可以表示为:$K_{i,j}=k(P^{i-1}z,P^{j-1}x)$。这里的$P$表示的是位移矩阵。易验证,$K^{z}$也是循环矩阵。
可计算得到各测试样本的响应值:

最后,我们可以求得一张feature map,也就是一张二维的响应图。


附录

这里补充在原论文的appendix中提到的两个问题。

  1. 余弦窗

    如果不加余弦窗,除了那个最原始样本,其他循环移位生成的样本的边缘都比较突兀,也就说这些样本数据是比较差的,会干扰训练的结果。而如果加了余弦窗。 上面是没加余弦窗的情况。除了那个最原始样本,其他样本都是合成的,如果不加余弦窗,那么这些合成样本就会降低分类器的判别能力(如上面c图所示,16张样本中只有两张比较合理)。如果加了余弦窗,就使得图像边缘像素值就都接近于或者等于0,因此循环移位过程中只要目标保持完整那这个样本就是合理的,只有当目标中心接近边缘时,目标跨越边界的那些样本才是错误的,这样以来虽不真实但合理的样本数量就可以增加到大约2/3。
  2. regression target y

    这个y是高斯加权后的值。初始目标的位置在padding后的search window的中心,循环移位得到的多个样本反应的是背景信息,而且离中心越远,就越不是目标,所以我们对标签进行高斯加权就刚好可以体现这种可能性准则。KCF里的输出是一个二维response矩阵,里面元素的大小代表该位置下的目标为预测目标的可能性,因此,在训练的时候就是输入是特征,而输出是一个gaussian_shaped_label,一般分类的标签是二值的,或者多值离散的,但是这个高斯标签反应的是由初始目标移位采样形成的若干样本距离初识样本越近可能性越大的准则,在代码中,高斯的峰值被移动到了左上角(于是四个角的值偏大),原因在论文的附录中进行了解释:“after computing a cross-correlation between two images in the Fourier domain and converting back to the spatial domain, it is the top-left element of the result that corresponds to a shift of zero”,也就是说目标零位移对应的是左上角的值。这样一来,我们在预测目标位置的时候,只需要pos=pos+find(response==max(response(:)))就好。如果把峰值放在中心点的话,就会“unnecessarily cause the detection output to be shifted by half a window”。

    补充:在代码中对目标进行padding是为了能让样本中含有特别需要学习的背景信息,而且可以尽量保证样本中目标的完整性,这是考虑循环移位将目标打散了。另外有关高斯加权,可以看一下我的文章computer-vision笔记:图像金字塔与高斯滤波器


效果

这是KCF在OTB2013上面做的一个实验,由于当时效果比较好的是struck(所以逃不了被针对的命运)。可以看到,KCF(使用HOG特征+高斯核函数)和DCF(也是同一个作者同一篇论文提出的,使用HOG特征+线性核函数,称为对偶相关滤波器)相比于struck来说,精度取得了显著的提升,从0.656提升到了0.732/0.728。

我们还可以根据这个统计图来看一下速度,即使使用了HOG特征和高斯核,KCF的速度还能达到172帧每秒。
此外,用了多通道扩展的DCF取得了更快的速度,但就精度而言较KCF稍差,但也是质的飞跃了。
相较而言,即使用了非常朴素的raw pixels,尽管效果比HOG特征差好多,但是速度并没有提高。这里也证明了HOG特征的强大。
另外可以看到KCF的祖宗MOSSE速度非常亮眼,但这是因为MOSSE它只用了简单的灰度特征,而不是HOG这样高维的特征,可想而知精确度总体效果还是要差一大截的。


缺点

  1. 缺点一

    对尺度变化的适应性不强。解决办法是加一个尺度变化的比例系数进行多次检测,代价是牺牲一些速度。
  2. 缺点二

    对目标快速变形(假设用的是HOG特征)或颜色快速变化(假设用的是颜色特征)不鲁棒。毕竟相关滤波是一种模板类的方法。HOG描述的就是形状信息,形状变化得太快必然会导致效果变差。而如果快速变色,那基于颜色特征的模板肯定也就跟不上了。这个还和模型更新策略与更新速度有关。若采用固定学习率的线性加权更新,那么如果学习率太大,部分或短暂遮挡和任何检测不准确,模型就会学习到背景信息,积累到一定程度模型被背景带飞了;如果学习率太小,目标已经变形了而模板还是那个模板,就会不认识目标,也会降低效果。
  3. 缺点三

    对物体快速运动或者低帧率视频不太鲁棒。这两种情况都是意味着在跟踪过程中下一帧图像中目标的位置偏离search window中心太远(要么靠近边缘,要么出去一半,要么全出去)。由于我们是给样本加了余弦窗的,也就是说目标位置靠近边缘会由于余弦窗的存在损失了部分目标信息(变成0),更不用说那些目标超出search window一半或者全超出去的情况了,这也就是CF类算法中的边界效应(Boundary Effects)。

其他

头一回看这么“理论”的论文我真的头都大了,要全部搞懂的话估计要花整整一天还不够。真的不得不佩服科研工作者们的智慧,我还是老老实实打基础吧。

原文的理论性、数学性更强,本文把主要的几个核心公式整理了一下,有些许修改和添加,如有疏漏还请多多指教。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:相关滤波与KCF

文章作者:高深远

发布时间:2020年01月20日 - 12:08

最后更新:2020年03月27日 - 23:19

原始链接:https://gsy00517.github.io/computer-vision20200120120823/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:anchor box | 高深远的博客

computer vision笔记:anchor box

之前在deep-learning笔记:着眼于深度——VGG简介与pytorch实现一文中提到了anchor box这一个概念,在我阅读YOLO的论文时,也遇到了这个名词。搞懂之后觉得还是写一下比较好。


bounding box的向量表示

在目标检测和目标跟踪等领域中,对于目标物体的位置和大小我们往往会使用bounding box来框出表示(有时为方便也简写为Bbox,注意不是那个打节拍的口技虽然我也在学哈哈)。下面以目标检测为例,介绍一下如何用向量表示bounding box。
我们一般取图片的左上角为(0,0),取图片的右下角为(1,1)。假如现在我们要对下面这个图像中的目标进行检测,检测该图中是否存在三大将即黄猿、赤犬、青雉。

我们可以使用这样一个8维的向量来表示图片中红色框即输出的bounding box。

其中$p_{c}$表示的是识别目标存在的概率,这里可以简化成1(存在目标)和0(不存在目标)。倘若$p_{c}=0$,也就是认为识别区域中不存在目标,那么向量中后面的7个参数就都是无意义项(don’t cares)。

注意:因为没有进行分割,这里的识别区域是整个图像,而在实际应用中往往进行了较为精细地分割以提高检测效果。

$b_{x}$和$b_{y}$指的是目标所在中心点的坐标,也就是bounding box的中点坐标。注意,这两个坐标的取值必须为0到1之间的数,且坐标系取图片的左上角为(0,0),取图片的右下角为(1,1),千万别搞错了。
$b_{h}$和$b_{w}$指的是bouding box占识别区域总长、宽的比值,在这个例子中取值在0到1之间,但要注意的是,它们的取值也可以超过1,也就是物体的大小超出了识别区域(这在分割图像后可能发生,可以看一下后文图例)。这就相当于以每个识别区域为单位1,我觉得这也是为什么之前设定坐标系时取右下角为(1,1)而不是其他数值的原因之一吧。
这个用于表示bounding box的向量的长度可以这样来计算:length = 5 + 待检测目标类别的总数。这时因为这里的$c_{1}c_{2}c_{3}$采用的是one-hot编码,当$p_{c}=1$时,这三个参数有且仅有一个值为1,即一个bounding box只能表示一个目标。比如bounding box中圈出的是赤犬,那么我们就可以将$c_{1}c_{2}c_{3}$表示为010。
以上文中的图片为例,其bounding box的向量表示应该如下所示。


anchor box

上文提到,我们可以将图像进行分割,以提高检测的效果。注意,这些分割是隐式的。在YOLO等algorithm中,一般有这样的规则,即物体的中心点在哪一个格子内,哪一个格子就负责检测这个物体。这就会导致一个问题。由于我们的检测方式是每一个识别区域(即分割的格子)都只输出一个向量表示其中的bounding box或者不存在目标($p_{c}=0$),因此当检测格子分割得较少时,可能会存在两个目标物体的中点同属一个格子而只能表示出一个物体的问题。这就需要引入anchor box。

比如在上图中,我们对图像进行3x3的分割,假如我们要识别汽车、人、汽车人这三类物体(也就是说要用到8维的向量),而汽车和人的中点都位于同一个格子中。这时再用之前的方法是无法同时识别人和汽车的。这时,我们可以预先定义两个不同形状的anchor box。

注意:在实际应用中往往会指定更多个anchor box。

此时,每个格子的输出向量就要表示成如下形式。此时向量的长度就要这样来计算:length = (5 + 待检测目标类别的总数) x anchor数

为了好看,我们用转置表示。

这里前8个参数是和竖着的anchor box1相关联的,后8个参数是和横着的anchor box2相关联的。
这样,anchor box1与人更近似,我们就用前8个参数标注人的bounding box,anchor box2与汽车更接近,我们就用后8个参数标注汽车的bounding box。此时这里的$b_{h}$和$b_{w}$指的就是anchor box之于格子的比值。因此,我们可以通过增加不同长宽比和尺寸的anchor box使得检测更加具有针对性。
倘若这张图片种只有汽车,那么我们选择与汽车的IoU(交并比)最大的anchor box。比如这里我们就是将第一个$p_{c}$设为0,随后的7个参数都成了无意义项,第二个$p_{c}$设为1,用最后的7个参数标注汽车也就是anchor box2。
一般情况下,我们可以通过更精细地分割图片来大大降低同一格子中同时出现两个物体的中点的可能。不过anchor box还有更多不错的效果值得借鉴,比如可以在目标跟踪中应对目标尺度大小的变化。


选取

关于如何选取anchor box,主要有三种方法:

  1. 人为经验选取

    当我们知道需要预测的目标类型时,我们往往可以合理地使用人工指定的方法设置一系列anchor box。比如设置扁而宽的用于检测汽车,设置长而窄的用于检测人,设置高而大的用于检测汽车人。
  2. 聚类

    这种方法在后期YOLO论文中有很好的使用,即利用机器学习中的k-means算法,将两类对象形状进行聚类,选择最具有代表性的一组anchor box来代表我们试图检测的一组对象类别。
  3. 作为参数学习

    即作为参数学习。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:anchor box

文章作者:高深远

发布时间:2020年01月28日 - 16:23

最后更新:2020年02月01日 - 21:22

原始链接:https://gsy00517.github.io/computer-vision20200128162333/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:anchor box | 高深远的博客

computer vision笔记:anchor box

之前在deep-learning笔记:着眼于深度——VGG简介与pytorch实现一文中提到了anchor box这一个概念,在我阅读YOLO的论文时,也遇到了这个名词。搞懂之后觉得还是写一下比较好。


bounding box的向量表示

在目标检测和目标跟踪等领域中,对于目标物体的位置和大小我们往往会使用bounding box来框出表示(有时为方便也简写为Bbox,注意不是那个打节拍的口技虽然我也在学哈哈)。下面以目标检测为例,介绍一下如何用向量表示bounding box。
我们一般取图片的左上角为(0,0),取图片的右下角为(1,1)。假如现在我们要对下面这个图像中的目标进行检测,检测该图中是否存在三大将即黄猿、赤犬、青雉。

我们可以使用这样一个8维的向量来表示图片中红色框即输出的bounding box。

其中$p_{c}$表示的是识别目标存在的概率,这里可以简化成1(存在目标)和0(不存在目标)。倘若$p_{c}=0$,也就是认为识别区域中不存在目标,那么向量中后面的7个参数就都是无意义项(don’t cares)。

注意:因为没有进行分割,这里的识别区域是整个图像,而在实际应用中往往进行了较为精细地分割以提高检测效果。

$b_{x}$和$b_{y}$指的是目标所在中心点的坐标,也就是bounding box的中点坐标。注意,这两个坐标的取值必须为0到1之间的数,且坐标系取图片的左上角为(0,0),取图片的右下角为(1,1),千万别搞错了。
$b_{h}$和$b_{w}$指的是bouding box占识别区域总长、宽的比值,在这个例子中取值在0到1之间,但要注意的是,它们的取值也可以超过1,也就是物体的大小超出了识别区域(这在分割图像后可能发生,可以看一下后文图例)。这就相当于以每个识别区域为单位1,我觉得这也是为什么之前设定坐标系时取右下角为(1,1)而不是其他数值的原因之一吧。
这个用于表示bounding box的向量的长度可以这样来计算:length = 5 + 待检测目标类别的总数。这时因为这里的$c_{1}c_{2}c_{3}$采用的是one-hot编码,当$p_{c}=1$时,这三个参数有且仅有一个值为1,即一个bounding box只能表示一个目标。比如bounding box中圈出的是赤犬,那么我们就可以将$c_{1}c_{2}c_{3}$表示为010。
以上文中的图片为例,其bounding box的向量表示应该如下所示。


anchor box

上文提到,我们可以将图像进行分割,以提高检测的效果。注意,这些分割是隐式的。在YOLO等algorithm中,一般有这样的规则,即物体的中心点在哪一个格子内,哪一个格子就负责检测这个物体。这就会导致一个问题。由于我们的检测方式是每一个识别区域(即分割的格子)都只输出一个向量表示其中的bounding box或者不存在目标($p_{c}=0$),因此当检测格子分割得较少时,可能会存在两个目标物体的中点同属一个格子而只能表示出一个物体的问题。这就需要引入anchor box。

比如在上图中,我们对图像进行3x3的分割,假如我们要识别汽车、人、汽车人这三类物体(也就是说要用到8维的向量),而汽车和人的中点都位于同一个格子中。这时再用之前的方法是无法同时识别人和汽车的。这时,我们可以预先定义两个不同形状的anchor box。

注意:在实际应用中往往会指定更多个anchor box。

此时,每个格子的输出向量就要表示成如下形式。此时向量的长度就要这样来计算:length = (5 + 待检测目标类别的总数) x anchor数

为了好看,我们用转置表示。

这里前8个参数是和竖着的anchor box1相关联的,后8个参数是和横着的anchor box2相关联的。
这样,anchor box1与人更近似,我们就用前8个参数标注人的bounding box,anchor box2与汽车更接近,我们就用后8个参数标注汽车的bounding box。此时这里的$b_{h}$和$b_{w}$指的就是anchor box之于格子的比值。因此,我们可以通过增加不同长宽比和尺寸的anchor box使得检测更加具有针对性。
倘若这张图片种只有汽车,那么我们选择与汽车的IoU(交并比)最大的anchor box。比如这里我们就是将第一个$p_{c}$设为0,随后的7个参数都成了无意义项,第二个$p_{c}$设为1,用最后的7个参数标注汽车也就是anchor box2。
一般情况下,我们可以通过更精细地分割图片来大大降低同一格子中同时出现两个物体的中点的可能。不过anchor box还有更多不错的效果值得借鉴,比如可以在目标跟踪中应对目标尺度大小的变化。


选取

关于如何选取anchor box,主要有三种方法:

  1. 人为经验选取

    当我们知道需要预测的目标类型时,我们往往可以合理地使用人工指定的方法设置一系列anchor box。比如设置扁而宽的用于检测汽车,设置长而窄的用于检测人,设置高而大的用于检测汽车人。
  2. 聚类

    这种方法在后期YOLO论文中有很好的使用,即利用机器学习中的k-means算法,将两类对象形状进行聚类,选择最具有代表性的一组anchor box来代表我们试图检测的一组对象类别。
  3. 作为参数学习

    即作为参数学习。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:anchor box

文章作者:高深远

发布时间:2020年01月28日 - 16:23

最后更新:2020年02月01日 - 21:22

原始链接:https://gsy00517.github.io/computer-vision20200128162333/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:non-max suppression | 高深远的博客

computer vision笔记:non-max suppression

Non-max Suppression即非极大值抑制,可简写为NMS,顾名思义就是抑制不是极大值,可以理解为局部最大搜索。由于在目标检测时,我们的算法可能会对同一个对象做出多次检测。我们的目标就是要去除冗余的检测框,仅保留最好的一个。这时就可以采用非极大值抑制的方法来确保算法对每个对象只检测一次。
如果本文阅读时有不懂之处,可以先看一下computer-vision笔记:anchor-box

References

电子文献:
https://www.cnblogs.com/makefile/p/nms.html


交并比(IoU)

或许在之前你已经看到过这个名词,比如computer-vision笔记:anchor-box。这里就简单介绍一下交并比。
可直接根据字面意思理解,下面直接通过一张图介绍,看完就懂。

实际上,IoU也是存在一定缺陷的,详见computer-vision笔记:IoU与GIoU


非极大值抑制(NMS)

非极大值抑制主要可以分为如下几步:

  1. 抛弃概率很低的预测

    这一步在网上大多数的文章中都没有被提及,但我认为是有必要的。因为有可能会存在着孤立的bounding box,它不会被抑制掉但它的概率很低,而它的内部的确没有框出目标,这是我们不希望的情况。因此非极大值抑制的第一步就是抛弃概率很低的预测,因为它们很有可能不包含任何目标。比如,我们可以抛弃$p_{c}< 0.5$的所有box。
  2. 选取概率最大的box并对其它box进行抑制

    在剩余的一系列box(记为$B$)中,选取概率$p_{c}$(这里是0到1之间的一个数)最大的box,并把它作为最终要输出的一个预测,从$B$中移除。
    同时我们从$B$中移除和刚刚选出的box的IoU达到一定阈值的box,因为它们很有可能在标注同一个目标。比如,我们可以把IoU大于阈值0.4的box都舍去。
  3. 重复直到列表为空

    重复第二步操作,也就是寻找剩余$B$中的下一个概率最大的box,并把它作为输出从$B$中移除,同时用它对它周围的box进行抑制。以此类推,直到$B$中不含未处理的box,即所有的有效预测均已输出。在图片所示的例子中,我们共需要进行两次循环,最终输出两个预测:人和汽车。

可用如下伪代码表示NMS的处理过程。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:non-max suppression

文章作者:高深远

发布时间:2020年01月28日 - 16:24

最后更新:2020年02月13日 - 23:25

原始链接:https://gsy00517.github.io/computer-vision20200128162422/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:non-max suppression | 高深远的博客

computer vision笔记:non-max suppression

Non-max Suppression即非极大值抑制,可简写为NMS,顾名思义就是抑制不是极大值,可以理解为局部最大搜索。由于在目标检测时,我们的算法可能会对同一个对象做出多次检测。我们的目标就是要去除冗余的检测框,仅保留最好的一个。这时就可以采用非极大值抑制的方法来确保算法对每个对象只检测一次。
如果本文阅读时有不懂之处,可以先看一下computer-vision笔记:anchor-box

References

电子文献:
https://www.cnblogs.com/makefile/p/nms.html


交并比(IoU)

或许在之前你已经看到过这个名词,比如computer-vision笔记:anchor-box。这里就简单介绍一下交并比。
可直接根据字面意思理解,下面直接通过一张图介绍,看完就懂。

实际上,IoU也是存在一定缺陷的,详见computer-vision笔记:IoU与GIoU


非极大值抑制(NMS)

非极大值抑制主要可以分为如下几步:

  1. 抛弃概率很低的预测

    这一步在网上大多数的文章中都没有被提及,但我认为是有必要的。因为有可能会存在着孤立的bounding box,它不会被抑制掉但它的概率很低,而它的内部的确没有框出目标,这是我们不希望的情况。因此非极大值抑制的第一步就是抛弃概率很低的预测,因为它们很有可能不包含任何目标。比如,我们可以抛弃$p_{c}< 0.5$的所有box。
  2. 选取概率最大的box并对其它box进行抑制

    在剩余的一系列box(记为$B$)中,选取概率$p_{c}$(这里是0到1之间的一个数)最大的box,并把它作为最终要输出的一个预测,从$B$中移除。
    同时我们从$B$中移除和刚刚选出的box的IoU达到一定阈值的box,因为它们很有可能在标注同一个目标。比如,我们可以把IoU大于阈值0.4的box都舍去。
  3. 重复直到列表为空

    重复第二步操作,也就是寻找剩余$B$中的下一个概率最大的box,并把它作为输出从$B$中移除,同时用它对它周围的box进行抑制。以此类推,直到$B$中不含未处理的box,即所有的有效预测均已输出。在图片所示的例子中,我们共需要进行两次循环,最终输出两个预测:人和汽车。

可用如下伪代码表示NMS的处理过程。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:non-max suppression

文章作者:高深远

发布时间:2020年01月28日 - 16:24

最后更新:2020年02月13日 - 23:25

原始链接:https://gsy00517.github.io/computer-vision20200128162422/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:RPN与Faster RCNN | 高深远的博客

computer vision笔记:RPN与Faster RCNN

最近在看SiamRPN系列,结果看着看着就看到Faster RCNN上面去了。尽管这个模型已经有一段时间了,我还是想把通过这两天学习的理解写下来。
RPN全称是Region Proposal Network,这里Region Proposal翻译为“区域选取”,也就是“提取候选框”的意思,所以RPN就是用来提取候选框的网络。在Faster RCNN这个结构中,RPN专门用来提取候选框,在RCNN和Fast RCNN等物体检测架构中,用来提取候选框的方法通常是比较传统的方法,而且比较耗时。而RPN一方面耗时较少,另一方面可以很容易结合到Fast RCNN中,成为一个整体。我们可以认为Faster RCNN所做的创新与改进就是用RPN结合Fast RCNN。它们三者都是based on regional proposal,即预先提取出候选区域,再通过CNN对候选区域进行样本分类(two-stage),这会影响它的速度,达不到YOLO那样的实时性,但从另一方面也保证了它的定位精度。

References

电子文献:
http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/
https://zhuanlan.zhihu.com/p/31426458
https://blog.csdn.net/lanran2/article/details/54376126


建议

在正文开始之前,推荐可以先了解一下相关的概念,比如1x1卷积、bounding box、anchor box和NMS等。我之前写过几篇相关的文章,可以先速览一下以防概念不清。
Bbox和anchor:computer-vision笔记:anchor-box
非极大抑制:computer-vision笔记:non-max suppression
1x1卷积:deep-learning笔记:着眼于深度——VGG简介与pytorch实现
此外也推荐b站上的两个视频图解RCNN和FastRCNN图解FasterRCNN,讲得挺不错的,就是声音有延迟,也可以直接看我的文章。视频中有地方讲得比较模糊,我在查阅资料之后更正了。
附faster RCNN的pytorch实现


RCNN和Fast RCNN

它们两者是Faster RCNN的先驱和基础,在这里简单介绍一下。首先先说明,无论是RCNN,还是Fast RCNN,还是Faster RCNN,它们的目的都是一样的,就是对一个图片,找出其中的目标物体,并用bounding box框出。

RCNN

RCNN即Region CNN,可以说是利用深度学习进行目标检测的开山之作,它用CNN(AlexNet)代替了之前sliding window的方法。

上面是RCNN的基本结构(图源自上面推荐的视频)。首先我们通过Selective Search(选择性搜索)去生成大量的认为可能存在目标物体的小图块。然后将所有的这些图块通过预先训练得很完美的CNN进行特征提取,比如AlexNet、VGG等。然后我们再对这些convNet卷积网络的输出进行两个操作:(1)Bbox回归,确定Bbox框出的目标位置即用回归的方式确定Bbox的4个参数;(2)分类,即用SVM判断Bbox标注的目标是什么物体。
RCNN的主要缺点就是计算成本非常巨大,这里会用上千张小图块去通过一个同样的卷积网络,即进行约2000次左右的串行式前向传播,而在之后又要分别经过回归和支持向量机两个模型,也就是要重复地执行以上操作上千遍,这就严重影响了速度。
由于使用了AlexNet(或者VGG),需要每一个候选框统一成相同的227x227的尺寸(若使用VGG,则是224x224),这就严重影响了CNN提取特征的质量。由于RCNN中SVM需要单独训练,随着类别的增加训练SVM的个数也会增加,这也使得训练过程更加复杂。此外,Selective Search去生成这些图像块的过程也是非常expensive和slow的。

补充:这里简单介绍一下Selective Search。
Selective Search类似于一种层次聚类算法,就是根据颜色、纹理、尺寸和空间交叠相似度,从众多小尺寸、细粒度的框中逐步选择、合并出大尺寸、粗粒度的约2000个候选框,用作随后的特征提取。

考虑到RCNN提取特征的巨大花费和较低的质量,后来何恺明大神对此做出了最早的改进,提出了SPP(空间金字塔池化),使得候选区域特征的提取只需要执行一遍且能够使任意大小的RoI统一成相同尺寸,其思路类似于稍晚于它的Fast RCNN(不过从日后的表现来看Fast RCNN的RoI Pooling较好),直到后来更好的Faster RCNN被提出。

Fast RCNN

Fast RCNN主要针对RCNN的问题进行了改进,即从逐个提取特征进步到了一次性提取特征。它首先直接对原始图像用卷积网络去提取特征,然后再在这张feature map上使用Selective Search。这样就使得只需要一张图像经过一遍卷积网络而不是将上千张图像去经过上千遍网络。然而Selective Search在这里还是没有得到改进,这是后面Faster RCNN使用RPN替换Selective Search的突破点。

然后通过RoI Pooling Layer使各个图像块的大小统一,以方便后面的回归和分类操作,这点后面还会讲到。最后Fast RCNN还做了一项改进就是使用两个并行的层代替了原本的SVM和Bbox回归两个模型,减少了模型的复杂度和参数量,同时这也将原本的分类、回归多任务损失统一在同一个框架中,相当于可以在训练的时候协同调整,使模型更加平衡,表现更好。


Faster RCNN

  1. 基本结构

    如图所示,Faster RCNN可以分为4个主要部分:
    1. conv layers

      作为一种CNN网络目标检测方法,Faster RCNN首先使用一组基础的conv卷积+relu激活+pooling池化提取image的feature maps。该feature maps被共享用于后续RPN层和全连接层。
    2. Region Proposal Network

      RPN网络用于生成region proposals。该层通过softmax判断anchors属于positive或者negative,再利用bounding box regression修正anchors获得精确的proposals。
    3. RoI pooling

      RoI即Region of Interest,RoI pooling是池化层的一种。该层收集输入的feature maps和proposals,综合这些信息后,提取proposal feature maps,送入后续全连接层判定目标类别。
    4. classifier

      利用proposal feature maps计算proposal的类别,即确定是什么物体。同时再次进行bounding box regression以获得检测框最终的精确位置。
  2. 流程

    1. 预处理

      首先对输入的图像进行预处理(常规操作),即减去均值并缩放成固定大小MxN。这个预处理过程对training和inference都是identical的。注意,这里的mean指的是对于整个训练集/测试集的均值而不是单张图片本身。
    2. 特征提取

      接收了处理后固定大小的图像后,使用一个卷积网络去提取特征,这个网络包含conv,pooling,relu三种层。以使用VGG16的Faster RCNN版本的网络结构为例,如图所示。 这里的conv layers部分共有13个conv层,13个relu层和4个pooling层。这里的参数值得注意:所有的conv层都设置kernel size为3,padding为1,stride为1;而所有的pooling层都设置kernel size为2且stride为2。
      这样设置有什么目的呢?可以结合下面的示意图来看,首先对所有的卷积都做了扩边处理(padding为1,即填充一圈0),使原图变为(M+2)x(N+2)的大小,然后再做3x3卷积,输出大小仍为MxN。正是这种设置,使得conv层不改变输入和输出矩阵大小。 再来看池化层,设kernel size为2且stride为2。这样每个经过pooling层的MxN矩阵,都会变为(M/2)x(N/2)大小。
      综上所述,在通过的整个卷积网络中,conv层和relu层不改变输入输出大小,只有pooling层使输出长宽都变为输入的1/2。那么4个pooling层就使得MxN大小的矩阵经过特征提取后固定变为(M/16)x(N/16)的feature maps。
    3. 提取候选框(RPN)

      接下来就是最重要的Region Proposal Network。经典的检测方法生成检测框都非常耗时,如OpenCV adaboost使用滑动窗口+图像金字塔生成检测框;或如RCNN使用SS(Selective Search)方法生成检测框。而Faster RCNN则抛弃了传统的滑动窗口和SS方法,直接使用RPN生成检测框,这也是Faster RCNN的巨大优势,能极大提升检测框的生成速度。
      这里还是先借用一张图来明确一下Faster RCNN的流程,整个RPN网络其实相当于这里的Anchor Generation Layer和Region Proposal Layer。

      这里的Anchor Target Layer是用于识别出一系列与ground truth box的分数达到一定阈值的较好的foreground anchors前景(物体)锚框和低于一定阈值的background anchors背景锚框,以及其对应的regression coefficients来训练RPN网络。这里的RPN Loss就是识别标记的foreground/background labels中的正确率与predicted和target regression coefficients之间的定义距离的组合。最后的Classification Loss也与RPN Loss定义类似,也是组合了正确率和系数距离。

      补充:这里我想先结合上面讲一下训练的过程,也可以跳过这一块继续看RPN。
      上面所述的Anchor Target Layer的输出并不用于后面分类器的训练。用于后面分类器训练的是Proposal Target Layer的输出。也就是说RPN层和分类器是分开训练的,先用预训练好的模型(比如VGG、ResNet和作者论文中用的ZF)训练RPN,再把训练好的RPN放到Faster RCNN中走上面流程图中的另一条路径训练分类器也就是整个Fast RCNN网络。根据这种思路,实际的训练过程分为4步:
      (1)在已经预训练好的model上,第一次训练RPN网络。
      (2)第一次训练整个Fast RCNN网络。
      (3)再第二次单独训练训练RPN网络。
      (4)再次利用步骤3中训练好的RPN网络,收集proposals,并第二次训练Fast RCNN网络。

      之所以只进行了类似的“循环”两次,是因为循环更多次并没有negligible improvements。

      好的还是先回到RPN模块。

      还是参照上一节提到的使用VGG16的Faster RCNN版本的网络结构,可以看到RPN网络实际分为2条线,上面一条通过softmax分类anchors获得positive anchors(存在目标的,也就是foreground anchors)和negative anchors两类,下面一条用于计算对于anchors的bounding box regression偏移量,以获得精确的proposal。而最后的Proposal Layer则负责综合positive anchors和对应bounding box regression偏移量获取proposals,同时剔除太小和超出边界的proposals。其实整个网络到了Proposal Layer这里,就相当于完成了目标定位的功能。
      下面更细地讲一下这里具体是怎么做的。

      anchor

      作者是这样生成anchor box的:对输入的feature map上的每一个点(pixel),都设置9个不同尺度和形状的anchor box,如下图所示。

      这样通过anchor box引入了目标检测中多尺度的方法,可以看到基本上整张图片上的各种尺度和形状都被cover到了。也可以事先通过对数据集聚类分析的方式来确定初始的anchor box的尺寸。
      当然,这样做获得box很不准确,不用担心,后面还有2次bounding box regression可以修正检测框的位置。

      补充:下面是原论文中的一张图,在这里做一些简单的解释。

      1. 这里的256-d指的是之前用于提取特征的卷积网络生成的feature maps的数量,其具体维度视使用的卷积网络而定。原文中使用的是ZF model中,其最后一层conv层输出维度为256,即生成256张feature maps,也相当于得到的一个feature map中每个点都是256维的。
      2. 结合前文中使用VGG16的Faster RCNN版本的网络结构,可以看到,在卷积网络提取出feature map之后,又做了3x3卷积且输出依旧是256维的,相当于每个点又融合了周围3x3的空间信息,也许这样做会是模型更鲁棒。
      3. 图中的k表示的不是千,而是每个点对应的anchor的个数,这里默认是9,而每个anhcor要分positive和negative,所以每个点cls分类需要两个分数,一个是前景(物体)分数,一个是背景分数,即图中所示2k scores;而每个anchor box又需要4个偏移量来定位,所以这里reg回归为4k coordinates。
      4. 在生成anchor的示意图中可以看到,显然anchors太多了,因此训练时会在合适的anchors中随机选取128个postive anchors与128个negative anchors进行训练。

      分类

      为了便于分析,我还是再把上面那张图拿下来。

      在经过3x3的卷积之后,又经过18个1x1的卷积核,这里的卷积主要是为了改变维数。比如我们输入一张WxH的feature map,那么经过该卷积输出就为WxHx18大小(输出图像通道数总是等于卷积核数量)。那么为什么是18呢?
      容易发现,18等于2(positive/negative)乘上9(anchors),也就是因为feature maps每一个点都有9个anchors,同时每个anchors又有可能是positive和negative,所以利用WxHx(9x2)大小的矩阵来保存这些信息。
      这里的softmax就是用于分类获得positive anchors,也就相当于初步提取了检测目标候选区域的Bbox。
      而softmax前后的两个reshape其实就是为了变换输入的张量以便于softmax分类(有点类似于一些网络在最后的卷积层和全连接层之间将张量拍扁成一维的),后面的reshape就是恢复原状的作用。
      其实RPN在这里就是在原图尺度上,设置了密密麻麻的候选anchor box。然后判断哪些是里面有目标的positive anchor,哪些是没目标的negative anchor。

      回归

      接下来我们来看看RPN模块下面那一条路径在做什么。
      如图所示,绿色框为事先标注的飞机的ground truth box(GT),红色框为前面提取的positive anchors,虽然红色框被分类器识别为飞机,但是由于红色框定位不准,依旧没有达到正确地检测出飞机的目标。所以我们希望采用一种方法对红色框进行微调,使得positive anchors和GT更加接近。

      对于一个box,我们一般使用一个四维向量$\left ( x,y,w,h \right )$来表示,即标注中心点的坐标和box的宽度和高度。在下图中,红色框A代表原始的positive anchors,绿色框G代表目标的GT,我们的目标是寻找一种关系,使得输入原始的A经过映射得到一个跟真实窗口G更接近的回归窗口G’,即寻找一种变换$F$,s.t.$F\left ( A_{x},A_{y},A_{w},A_{h} \right )=\left ( G_{x}^{‘},G_{y}^{‘},G_{w}^{‘},G_{h}^{‘} \right )$且$\left ( G_{x}^{‘},G_{y}^{‘},G_{w}^{‘},G_{h}^{‘} \right )\approx \left ( G_{x},G_{y},G_{w},G_{h} \right )$。

      那么这个变换$F$如何选择呢?一种简单的思路就是先做平移后做缩放,即:

      因此我们需要学习的是$d_{x}\left ( A \right )$,$d_{y}\left ( A \right )$,$d_{w}\left ( A \right )$,$d_{h}\left ( A \right )$这四个变换。当输入的A与GT相差较小时,可以认为这种变换是一种线性变换,那么就可以用线性回归来进行微调。

      注:线性回归就是给定输入的特征向量$X$,学习一组参数$W$,使得经过线性回归后的值跟真实值$Y$非常接近,即$Y=WX$。

      对于该问题,输入$X$是cnn feature map,定义为$\phi$;同时还有训练传入A与GT之间的变换量,即$\left ( t_{x},t_{y},t_{w},t_{h} \right )$。输出是上面所说的四个变换。则目标函数可表示为:

      为了让预测值$d_{\ast }\left ( A \right )$与真实值$t_{\ast }$差距最小,设计L1损失函数:

      得到优化目标为:

      这里的$argmin$表示的是给定参数的表达式达到最小值。

      补充:这里positive anchor与ground truth之间的平移量$\left ( t_{x},t_{y} \right )$和尺度因子$\left ( t_{w},t_{h} \right )$定义如下:

      请结合下图理解。之所以这样定义,是为了回归系数在图片进行仿射变换之后依旧能够保持不变。

      有关仿射变换,可以看一下之前的文章linear-algebra笔记:二维仿射变换

      Proposal Layer

      Proposal Layer有3个输入:positive anchors分类器结果,Bbox reg的变换量以及生成的anchor。如图所示,在选择最优的多个box,然后对这些box作NMS,结果作为proposals输出。

      RPN小结

      以上就是RPN网络提取候选框的大致介绍,总结起来就是:首先,生成anchors;然后,用softmax分类器提取positvie anchors;接着,Bbox reg回归positive anchors;最后,通过Proposal Layer生成proposals。

    4. RoI pooling

      先来看一个问题:对于传统的CNN(如AlexNet和VGG),当网络训练好后输入的图像尺寸必须是固定值,同时网络输出也是固定大小的vector或者matrix。如果输入图像大小不定,这个问题就变得比较麻烦。有2种解决办法:从图像中crop一部分传入网络,或者将图像warp成需要的大小后传入网络。 问题是,无论采取那种办法都不是很好,要么crop后破坏了图像的完整结构,要么warp后破坏了图像原始形状信息。
      于是,Fast RCNN就提出了RoI pooling来解决这个问题,其思路是与其wrap图像破坏信息,不如尝试着去wrap特征。
      其步骤如下:

      Step1:Coordinate on image

      由于proposal对应的尺度是MXN,所以首先将其映射回(M/16)X(N/16)尺度大小的feature map。(若不能整除,则向下取整,相当于丢弃小部分右侧和下侧的信息)

      Step2:Coordinate on feature map

      再将每个proposal对应的feature map区域水平分为WxH的网格。

      Step3:Coordinate on RoI feature

      接着对网格中的每一份都进行max pooling处理。如此就得到了WxH固定大小的输出。 由于RoI pooling这里采用了两次浮点数取整运算,这就使得池化之后的输出可能会于原图像的尺寸对不上,因此后来何恺明大神又提出了基于双线性插值的RoI Align来代替取整,作出了进一步改进。

      补充:2019年IoUNet又提出了PrRoI pooling,相比RoI Align,它不用预设N的个数,直接使用积分取均值。

    5. 分类器

      最后的分类部分利用已经获得的proposal feature maps,通过full connect层与softmax计算每个proposal具体属于那个类别(如人,车,电视等),输出含有各个类别的概率向量;同时再次利用bounding box regression获得每个proposal的位置偏移量,用于回归更加精确的目标检测框。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:RPN与Faster RCNN

文章作者:高深远

发布时间:2020年01月29日 - 10:29

最后更新:2020年03月20日 - 12:18

原始链接:https://gsy00517.github.io/computer-vision20200129102927/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:RPN与Faster RCNN | 高深远的博客

computer vision笔记:RPN与Faster RCNN

最近在看SiamRPN系列,结果看着看着就看到Faster RCNN上面去了。尽管这个模型已经有一段时间了,我还是想把通过这两天学习的理解写下来。
RPN全称是Region Proposal Network,这里Region Proposal翻译为“区域选取”,也就是“提取候选框”的意思,所以RPN就是用来提取候选框的网络。在Faster RCNN这个结构中,RPN专门用来提取候选框,在RCNN和Fast RCNN等物体检测架构中,用来提取候选框的方法通常是比较传统的方法,而且比较耗时。而RPN一方面耗时较少,另一方面可以很容易结合到Fast RCNN中,成为一个整体。我们可以认为Faster RCNN所做的创新与改进就是用RPN结合Fast RCNN。它们三者都是based on regional proposal,即预先提取出候选区域,再通过CNN对候选区域进行样本分类(two-stage),这会影响它的速度,达不到YOLO那样的实时性,但从另一方面也保证了它的定位精度。

References

电子文献:
http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/
https://zhuanlan.zhihu.com/p/31426458
https://blog.csdn.net/lanran2/article/details/54376126


建议

在正文开始之前,推荐可以先了解一下相关的概念,比如1x1卷积、bounding box、anchor box和NMS等。我之前写过几篇相关的文章,可以先速览一下以防概念不清。
Bbox和anchor:computer-vision笔记:anchor-box
非极大抑制:computer-vision笔记:non-max suppression
1x1卷积:deep-learning笔记:着眼于深度——VGG简介与pytorch实现
此外也推荐b站上的两个视频图解RCNN和FastRCNN图解FasterRCNN,讲得挺不错的,就是声音有延迟,也可以直接看我的文章。视频中有地方讲得比较模糊,我在查阅资料之后更正了。
附faster RCNN的pytorch实现


RCNN和Fast RCNN

它们两者是Faster RCNN的先驱和基础,在这里简单介绍一下。首先先说明,无论是RCNN,还是Fast RCNN,还是Faster RCNN,它们的目的都是一样的,就是对一个图片,找出其中的目标物体,并用bounding box框出。

RCNN

RCNN即Region CNN,可以说是利用深度学习进行目标检测的开山之作,它用CNN(AlexNet)代替了之前sliding window的方法。

上面是RCNN的基本结构(图源自上面推荐的视频)。首先我们通过Selective Search(选择性搜索)去生成大量的认为可能存在目标物体的小图块。然后将所有的这些图块通过预先训练得很完美的CNN进行特征提取,比如AlexNet、VGG等。然后我们再对这些convNet卷积网络的输出进行两个操作:(1)Bbox回归,确定Bbox框出的目标位置即用回归的方式确定Bbox的4个参数;(2)分类,即用SVM判断Bbox标注的目标是什么物体。
RCNN的主要缺点就是计算成本非常巨大,这里会用上千张小图块去通过一个同样的卷积网络,即进行约2000次左右的串行式前向传播,而在之后又要分别经过回归和支持向量机两个模型,也就是要重复地执行以上操作上千遍,这就严重影响了速度。
由于使用了AlexNet(或者VGG),需要每一个候选框统一成相同的227x227的尺寸(若使用VGG,则是224x224),这就严重影响了CNN提取特征的质量。由于RCNN中SVM需要单独训练,随着类别的增加训练SVM的个数也会增加,这也使得训练过程更加复杂。此外,Selective Search去生成这些图像块的过程也是非常expensive和slow的。

补充:这里简单介绍一下Selective Search。
Selective Search类似于一种层次聚类算法,就是根据颜色、纹理、尺寸和空间交叠相似度,从众多小尺寸、细粒度的框中逐步选择、合并出大尺寸、粗粒度的约2000个候选框,用作随后的特征提取。

考虑到RCNN提取特征的巨大花费和较低的质量,后来何恺明大神对此做出了最早的改进,提出了SPP(空间金字塔池化),使得候选区域特征的提取只需要执行一遍且能够使任意大小的RoI统一成相同尺寸,其思路类似于稍晚于它的Fast RCNN(不过从日后的表现来看Fast RCNN的RoI Pooling较好),直到后来更好的Faster RCNN被提出。

Fast RCNN

Fast RCNN主要针对RCNN的问题进行了改进,即从逐个提取特征进步到了一次性提取特征。它首先直接对原始图像用卷积网络去提取特征,然后再在这张feature map上使用Selective Search。这样就使得只需要一张图像经过一遍卷积网络而不是将上千张图像去经过上千遍网络。然而Selective Search在这里还是没有得到改进,这是后面Faster RCNN使用RPN替换Selective Search的突破点。

然后通过RoI Pooling Layer使各个图像块的大小统一,以方便后面的回归和分类操作,这点后面还会讲到。最后Fast RCNN还做了一项改进就是使用两个并行的层代替了原本的SVM和Bbox回归两个模型,减少了模型的复杂度和参数量,同时这也将原本的分类、回归多任务损失统一在同一个框架中,相当于可以在训练的时候协同调整,使模型更加平衡,表现更好。


Faster RCNN

  1. 基本结构

    如图所示,Faster RCNN可以分为4个主要部分:
    1. conv layers

      作为一种CNN网络目标检测方法,Faster RCNN首先使用一组基础的conv卷积+relu激活+pooling池化提取image的feature maps。该feature maps被共享用于后续RPN层和全连接层。
    2. Region Proposal Network

      RPN网络用于生成region proposals。该层通过softmax判断anchors属于positive或者negative,再利用bounding box regression修正anchors获得精确的proposals。
    3. RoI pooling

      RoI即Region of Interest,RoI pooling是池化层的一种。该层收集输入的feature maps和proposals,综合这些信息后,提取proposal feature maps,送入后续全连接层判定目标类别。
    4. classifier

      利用proposal feature maps计算proposal的类别,即确定是什么物体。同时再次进行bounding box regression以获得检测框最终的精确位置。
  2. 流程

    1. 预处理

      首先对输入的图像进行预处理(常规操作),即减去均值并缩放成固定大小MxN。这个预处理过程对training和inference都是identical的。注意,这里的mean指的是对于整个训练集/测试集的均值而不是单张图片本身。
    2. 特征提取

      接收了处理后固定大小的图像后,使用一个卷积网络去提取特征,这个网络包含conv,pooling,relu三种层。以使用VGG16的Faster RCNN版本的网络结构为例,如图所示。 这里的conv layers部分共有13个conv层,13个relu层和4个pooling层。这里的参数值得注意:所有的conv层都设置kernel size为3,padding为1,stride为1;而所有的pooling层都设置kernel size为2且stride为2。
      这样设置有什么目的呢?可以结合下面的示意图来看,首先对所有的卷积都做了扩边处理(padding为1,即填充一圈0),使原图变为(M+2)x(N+2)的大小,然后再做3x3卷积,输出大小仍为MxN。正是这种设置,使得conv层不改变输入和输出矩阵大小。 再来看池化层,设kernel size为2且stride为2。这样每个经过pooling层的MxN矩阵,都会变为(M/2)x(N/2)大小。
      综上所述,在通过的整个卷积网络中,conv层和relu层不改变输入输出大小,只有pooling层使输出长宽都变为输入的1/2。那么4个pooling层就使得MxN大小的矩阵经过特征提取后固定变为(M/16)x(N/16)的feature maps。
    3. 提取候选框(RPN)

      接下来就是最重要的Region Proposal Network。经典的检测方法生成检测框都非常耗时,如OpenCV adaboost使用滑动窗口+图像金字塔生成检测框;或如RCNN使用SS(Selective Search)方法生成检测框。而Faster RCNN则抛弃了传统的滑动窗口和SS方法,直接使用RPN生成检测框,这也是Faster RCNN的巨大优势,能极大提升检测框的生成速度。
      这里还是先借用一张图来明确一下Faster RCNN的流程,整个RPN网络其实相当于这里的Anchor Generation Layer和Region Proposal Layer。

      这里的Anchor Target Layer是用于识别出一系列与ground truth box的分数达到一定阈值的较好的foreground anchors前景(物体)锚框和低于一定阈值的background anchors背景锚框,以及其对应的regression coefficients来训练RPN网络。这里的RPN Loss就是识别标记的foreground/background labels中的正确率与predicted和target regression coefficients之间的定义距离的组合。最后的Classification Loss也与RPN Loss定义类似,也是组合了正确率和系数距离。

      补充:这里我想先结合上面讲一下训练的过程,也可以跳过这一块继续看RPN。
      上面所述的Anchor Target Layer的输出并不用于后面分类器的训练。用于后面分类器训练的是Proposal Target Layer的输出。也就是说RPN层和分类器是分开训练的,先用预训练好的模型(比如VGG、ResNet和作者论文中用的ZF)训练RPN,再把训练好的RPN放到Faster RCNN中走上面流程图中的另一条路径训练分类器也就是整个Fast RCNN网络。根据这种思路,实际的训练过程分为4步:
      (1)在已经预训练好的model上,第一次训练RPN网络。
      (2)第一次训练整个Fast RCNN网络。
      (3)再第二次单独训练训练RPN网络。
      (4)再次利用步骤3中训练好的RPN网络,收集proposals,并第二次训练Fast RCNN网络。

      之所以只进行了类似的“循环”两次,是因为循环更多次并没有negligible improvements。

      好的还是先回到RPN模块。

      还是参照上一节提到的使用VGG16的Faster RCNN版本的网络结构,可以看到RPN网络实际分为2条线,上面一条通过softmax分类anchors获得positive anchors(存在目标的,也就是foreground anchors)和negative anchors两类,下面一条用于计算对于anchors的bounding box regression偏移量,以获得精确的proposal。而最后的Proposal Layer则负责综合positive anchors和对应bounding box regression偏移量获取proposals,同时剔除太小和超出边界的proposals。其实整个网络到了Proposal Layer这里,就相当于完成了目标定位的功能。
      下面更细地讲一下这里具体是怎么做的。

      anchor

      作者是这样生成anchor box的:对输入的feature map上的每一个点(pixel),都设置9个不同尺度和形状的anchor box,如下图所示。

      这样通过anchor box引入了目标检测中多尺度的方法,可以看到基本上整张图片上的各种尺度和形状都被cover到了。也可以事先通过对数据集聚类分析的方式来确定初始的anchor box的尺寸。
      当然,这样做获得box很不准确,不用担心,后面还有2次bounding box regression可以修正检测框的位置。

      补充:下面是原论文中的一张图,在这里做一些简单的解释。

      1. 这里的256-d指的是之前用于提取特征的卷积网络生成的feature maps的数量,其具体维度视使用的卷积网络而定。原文中使用的是ZF model中,其最后一层conv层输出维度为256,即生成256张feature maps,也相当于得到的一个feature map中每个点都是256维的。
      2. 结合前文中使用VGG16的Faster RCNN版本的网络结构,可以看到,在卷积网络提取出feature map之后,又做了3x3卷积且输出依旧是256维的,相当于每个点又融合了周围3x3的空间信息,也许这样做会是模型更鲁棒。
      3. 图中的k表示的不是千,而是每个点对应的anchor的个数,这里默认是9,而每个anhcor要分positive和negative,所以每个点cls分类需要两个分数,一个是前景(物体)分数,一个是背景分数,即图中所示2k scores;而每个anchor box又需要4个偏移量来定位,所以这里reg回归为4k coordinates。
      4. 在生成anchor的示意图中可以看到,显然anchors太多了,因此训练时会在合适的anchors中随机选取128个postive anchors与128个negative anchors进行训练。

      分类

      为了便于分析,我还是再把上面那张图拿下来。

      在经过3x3的卷积之后,又经过18个1x1的卷积核,这里的卷积主要是为了改变维数。比如我们输入一张WxH的feature map,那么经过该卷积输出就为WxHx18大小(输出图像通道数总是等于卷积核数量)。那么为什么是18呢?
      容易发现,18等于2(positive/negative)乘上9(anchors),也就是因为feature maps每一个点都有9个anchors,同时每个anchors又有可能是positive和negative,所以利用WxHx(9x2)大小的矩阵来保存这些信息。
      这里的softmax就是用于分类获得positive anchors,也就相当于初步提取了检测目标候选区域的Bbox。
      而softmax前后的两个reshape其实就是为了变换输入的张量以便于softmax分类(有点类似于一些网络在最后的卷积层和全连接层之间将张量拍扁成一维的),后面的reshape就是恢复原状的作用。
      其实RPN在这里就是在原图尺度上,设置了密密麻麻的候选anchor box。然后判断哪些是里面有目标的positive anchor,哪些是没目标的negative anchor。

      回归

      接下来我们来看看RPN模块下面那一条路径在做什么。
      如图所示,绿色框为事先标注的飞机的ground truth box(GT),红色框为前面提取的positive anchors,虽然红色框被分类器识别为飞机,但是由于红色框定位不准,依旧没有达到正确地检测出飞机的目标。所以我们希望采用一种方法对红色框进行微调,使得positive anchors和GT更加接近。

      对于一个box,我们一般使用一个四维向量$\left ( x,y,w,h \right )$来表示,即标注中心点的坐标和box的宽度和高度。在下图中,红色框A代表原始的positive anchors,绿色框G代表目标的GT,我们的目标是寻找一种关系,使得输入原始的A经过映射得到一个跟真实窗口G更接近的回归窗口G’,即寻找一种变换$F$,s.t.$F\left ( A_{x},A_{y},A_{w},A_{h} \right )=\left ( G_{x}^{‘},G_{y}^{‘},G_{w}^{‘},G_{h}^{‘} \right )$且$\left ( G_{x}^{‘},G_{y}^{‘},G_{w}^{‘},G_{h}^{‘} \right )\approx \left ( G_{x},G_{y},G_{w},G_{h} \right )$。

      那么这个变换$F$如何选择呢?一种简单的思路就是先做平移后做缩放,即:

      因此我们需要学习的是$d_{x}\left ( A \right )$,$d_{y}\left ( A \right )$,$d_{w}\left ( A \right )$,$d_{h}\left ( A \right )$这四个变换。当输入的A与GT相差较小时,可以认为这种变换是一种线性变换,那么就可以用线性回归来进行微调。

      注:线性回归就是给定输入的特征向量$X$,学习一组参数$W$,使得经过线性回归后的值跟真实值$Y$非常接近,即$Y=WX$。

      对于该问题,输入$X$是cnn feature map,定义为$\phi$;同时还有训练传入A与GT之间的变换量,即$\left ( t_{x},t_{y},t_{w},t_{h} \right )$。输出是上面所说的四个变换。则目标函数可表示为:

      为了让预测值$d_{\ast }\left ( A \right )$与真实值$t_{\ast }$差距最小,设计L1损失函数:

      得到优化目标为:

      这里的$argmin$表示的是给定参数的表达式达到最小值。

      补充:这里positive anchor与ground truth之间的平移量$\left ( t_{x},t_{y} \right )$和尺度因子$\left ( t_{w},t_{h} \right )$定义如下:

      请结合下图理解。之所以这样定义,是为了回归系数在图片进行仿射变换之后依旧能够保持不变。

      有关仿射变换,可以看一下之前的文章linear-algebra笔记:二维仿射变换

      Proposal Layer

      Proposal Layer有3个输入:positive anchors分类器结果,Bbox reg的变换量以及生成的anchor。如图所示,在选择最优的多个box,然后对这些box作NMS,结果作为proposals输出。

      RPN小结

      以上就是RPN网络提取候选框的大致介绍,总结起来就是:首先,生成anchors;然后,用softmax分类器提取positvie anchors;接着,Bbox reg回归positive anchors;最后,通过Proposal Layer生成proposals。

    4. RoI pooling

      先来看一个问题:对于传统的CNN(如AlexNet和VGG),当网络训练好后输入的图像尺寸必须是固定值,同时网络输出也是固定大小的vector或者matrix。如果输入图像大小不定,这个问题就变得比较麻烦。有2种解决办法:从图像中crop一部分传入网络,或者将图像warp成需要的大小后传入网络。 问题是,无论采取那种办法都不是很好,要么crop后破坏了图像的完整结构,要么warp后破坏了图像原始形状信息。
      于是,Fast RCNN就提出了RoI pooling来解决这个问题,其思路是与其wrap图像破坏信息,不如尝试着去wrap特征。
      其步骤如下:

      Step1:Coordinate on image

      由于proposal对应的尺度是MXN,所以首先将其映射回(M/16)X(N/16)尺度大小的feature map。(若不能整除,则向下取整,相当于丢弃小部分右侧和下侧的信息)

      Step2:Coordinate on feature map

      再将每个proposal对应的feature map区域水平分为WxH的网格。

      Step3:Coordinate on RoI feature

      接着对网格中的每一份都进行max pooling处理。如此就得到了WxH固定大小的输出。 由于RoI pooling这里采用了两次浮点数取整运算,这就使得池化之后的输出可能会于原图像的尺寸对不上,因此后来何恺明大神又提出了基于双线性插值的RoI Align来代替取整,作出了进一步改进。

      补充:2019年IoUNet又提出了PrRoI pooling,相比RoI Align,它不用预设N的个数,直接使用积分取均值。

    5. 分类器

      最后的分类部分利用已经获得的proposal feature maps,通过full connect层与softmax计算每个proposal具体属于那个类别(如人,车,电视等),输出含有各个类别的概率向量;同时再次利用bounding box regression获得每个proposal的位置偏移量,用于回归更加精确的目标检测框。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:RPN与Faster RCNN

文章作者:高深远

发布时间:2020年01月29日 - 10:29

最后更新:2020年03月20日 - 12:18

原始链接:https://gsy00517.github.io/computer-vision20200129102927/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:AP和mAP | 高深远的博客

computer vision笔记:AP和mAP

本文紧接前一篇文章machine-learning笔记:准确率和召回率,具体到目标检测中来谈一谈两个指标:AP和mAP。AP即average precision平均准确率,而mAP即mean average precision,翻译成平均平均准确率…不太好听,我且称它为均匀平均准确率吧。


目标检测中的P和R

在目标检测中,TP、FP、FN、TN有更加具体的定义,比如我们可以这样定义:
TP:IoU大于或等于阈值的检测框。
FP:IoU小于阈值的检测框。
FN:未被检测到的(没有被标注的)ground truth。
TN:由于在实验中一般不会去标注负类,因此不关心这项,可以忽略不计。
关于IoU,也就是交集比上并集的比值,如果没理解可以看看computer-vision笔记:non-max suppression这篇文章。


AP

比如有这样一个例子,总共有7个图像,其中用绿色框表示15个ground truth,用红色框表示24个我们预测的对象,并用字母标记和给出confidence。

补充:在YOLO中,confidence是这样定义的:

这里的$Pr(Object)$指的是物体先验概率,如果框内包含物体则其值为1,否则其值为0。$IoU_{pred}^{truth}$为预测框与GTbox的交并比,取值在0到1之间。因此confidence的取值范围也在0到1之间。
由该公式可知confidence反应了两个信息:

  1. 预测框内包含物体的置信度。
  2. 预测框预测的准确度。

然后我们用上面的方法判断每一个标注出来的预测对象预测得是正确的(TP)还是不正确的(FP),并根据confidence从大到小进行排列。注意,这里的Acc TP和Acc FP指的是累计的正确个数和错误个数。准确率是利用累计的正确个数比上已经统计的个数,召回率是利用累计的正确个数去比上总的正确个数(也就是15)。至于为什么这么计算,可以看看我前一篇文章的理解。

在统计完所有预测红框的准确率和召回率之后,接下来我们计算AP。利用前面计算得出的数据,我们可以画出如下统计图。

上面这种方法称为11点插值法,就是从召回率为0开始,以0.1为间隔计算准确率,因此共有11项。
如上图右下角的公式所示,我们定义这11个点中每一个点的准确率等于大于该召回率的所有点中准确率的最大值。最后累加求平均即为该分类的AP值。这种方法的缺点是它的估算有点粗糙,因此后来作了如下改进。在2012年之后(还好没有世界末日),又出现了用面积来逼近AP值的方法。

如上图中的公式所示,这里做得改变就是把插值点的精确度替换成了当前点的精确度,也就是把离散变成连续,从而可以计算面积,使得最后的平均更加有意义。


mAP

以上就是AP的两种计算方法,那么怎么来计算mAP呢?
其实AP针对的是某一类比如说人,而数据集中往往还有其他的许多类别比如说汽车、汽车人。那么分别计算每一个类的AP再求平均就得到了mAP。
注意,mAP针对的是多分类任务。mAP越高,代表精度越高。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:AP和mAP

文章作者:高深远

发布时间:2020年01月30日 - 13:04

最后更新:2020年02月01日 - 21:24

原始链接:https://gsy00517.github.io/computer-vision20200130130437/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:AP和mAP | 高深远的博客

computer vision笔记:AP和mAP

本文紧接前一篇文章machine-learning笔记:准确率和召回率,具体到目标检测中来谈一谈两个指标:AP和mAP。AP即average precision平均准确率,而mAP即mean average precision,翻译成平均平均准确率…不太好听,我且称它为均匀平均准确率吧。


目标检测中的P和R

在目标检测中,TP、FP、FN、TN有更加具体的定义,比如我们可以这样定义:
TP:IoU大于或等于阈值的检测框。
FP:IoU小于阈值的检测框。
FN:未被检测到的(没有被标注的)ground truth。
TN:由于在实验中一般不会去标注负类,因此不关心这项,可以忽略不计。
关于IoU,也就是交集比上并集的比值,如果没理解可以看看computer-vision笔记:non-max suppression这篇文章。


AP

比如有这样一个例子,总共有7个图像,其中用绿色框表示15个ground truth,用红色框表示24个我们预测的对象,并用字母标记和给出confidence。

补充:在YOLO中,confidence是这样定义的:

这里的$Pr(Object)$指的是物体先验概率,如果框内包含物体则其值为1,否则其值为0。$IoU_{pred}^{truth}$为预测框与GTbox的交并比,取值在0到1之间。因此confidence的取值范围也在0到1之间。
由该公式可知confidence反应了两个信息:

  1. 预测框内包含物体的置信度。
  2. 预测框预测的准确度。

然后我们用上面的方法判断每一个标注出来的预测对象预测得是正确的(TP)还是不正确的(FP),并根据confidence从大到小进行排列。注意,这里的Acc TP和Acc FP指的是累计的正确个数和错误个数。准确率是利用累计的正确个数比上已经统计的个数,召回率是利用累计的正确个数去比上总的正确个数(也就是15)。至于为什么这么计算,可以看看我前一篇文章的理解。

在统计完所有预测红框的准确率和召回率之后,接下来我们计算AP。利用前面计算得出的数据,我们可以画出如下统计图。

上面这种方法称为11点插值法,就是从召回率为0开始,以0.1为间隔计算准确率,因此共有11项。
如上图右下角的公式所示,我们定义这11个点中每一个点的准确率等于大于该召回率的所有点中准确率的最大值。最后累加求平均即为该分类的AP值。这种方法的缺点是它的估算有点粗糙,因此后来作了如下改进。在2012年之后(还好没有世界末日),又出现了用面积来逼近AP值的方法。

如上图中的公式所示,这里做得改变就是把插值点的精确度替换成了当前点的精确度,也就是把离散变成连续,从而可以计算面积,使得最后的平均更加有意义。


mAP

以上就是AP的两种计算方法,那么怎么来计算mAP呢?
其实AP针对的是某一类比如说人,而数据集中往往还有其他的许多类别比如说汽车、汽车人。那么分别计算每一个类的AP再求平均就得到了mAP。
注意,mAP针对的是多分类任务。mAP越高,代表精度越高。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:AP和mAP

文章作者:高深远

发布时间:2020年01月30日 - 13:04

最后更新:2020年02月01日 - 21:24

原始链接:https://gsy00517.github.io/computer-vision20200130130437/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:将检测问题变回归问题——YOLO | 高深远的博客

computer vision笔记:将检测问题变回归问题——YOLO

在许多新的算法里,涉及到bounding box时,往往会转化成回归问题。而做出这种转变的开山之作,据我所知应该是YOLO。关于YOLO,网上有很多很好的解读,我也在之前的文章中多次提到过YOLO的一些要素,这里不一一列举了,也不重复了,可以使用我网站内的搜索功能看一下相关内容。Andrew Ng在卷积神经网络的公开课上好像也特地围绕YOLO的相关内容讲了一周吧,也可以去看看。本文主要是想记录一下YOLO设计损失函数的一点trick和一些理解。
注意,本文讨论的是YOLOv1。


损失函数

我们直接看YOLO的损失函数。

可以看到,这里所有的损失都是利用ground truth和预测作差。熟悉机器学习相关知识的读者应该会知道,一般我们计算分类的损失时,一般会使用交叉熵或者逻辑回归,而YOLO的类别损失(上图最后一项)依旧使用了直接作差的方法,所以我们说,YOLO把整个检测问题作为一个回归问题来处理。
暂时不关注其它损失项,我觉得这里比较巧妙的是第二项也就是宽高误差。善于观察的话肯定会发现,宽高误差在这里所有的损失项中略显突兀,即它在作差之前分别对ground truth和预测值开了根号,这是为什么呢?
实际上,YOLO的一个motivation就是解决密集物体和小物体的检测(虽然还是没能很好的解决)。相比于大的Bbox产生预测误差,作者更加关注小的Bbox产生的误差。而平方误差损失(sum-squared error loss)中对同样的偏移loss值是一样的。因此这里用了一种比较巧妙的办法,就是用长和宽的平方根来代替原值,这时小的Bbox发生偏移时,其横轴上反应的loss比大的Bbox发生偏移时要大。

这为我们提供了一种很好的思路,若我们对大跨度的目标更感兴趣或者说更希望较大的目标预测更精确时,我们不妨也可以使用平方或是立方来增强大尺度时的反应。其实我觉得这里的思想其实和Gamma校正是一致的。


YOLO优点

  1. 速度快且容易优化

    YOLO(You Only Look Once)只需读取一次图像就可以进行端到端的优化,这使得它的速度可以很快。它将检测问题转化为回归问题,可以满足实时性的要求。我手机上的一款APP用的就是YOLO的改进版。
  2. 背景误识别率低

    YOLO对全图进行卷积学习,综合考虑了全图的上下文信息,因此背景的误检测较少。
  3. 泛化能力强

    这也是因为综合考虑了图片全局,因此能够更好地学习数据集的本质表达,从而使得泛化性能更好。

YOLO缺点

  1. 定位精度不够

    这尤其是对于小目标,因为网络较深,使得细粒度特征和小物体特征不明显。因为它是端到端的检测算法(one-stage),没有Faster RCNN那样提取候选框的操作,这也导致了它的精度不高。

    补充:由于one-stage和two-stage检测都存在着速度与精度平衡的问题,所以后来的SSD(也是端到端)在借鉴YOLO的网络架构和Faster RCNN中的anchor box实现多尺度的思想的基础上,设计出了更好的算法。可以看一下computer-vision笔记:SSD和DSSD

  2. 对密集目标的识别存在不足

    这主要是因为一个grid cell只能预测一个物体。如下图所示,当两个bicycle靠得比较近时,只预测出了一个bicycle。

  3. 异常长宽比的目标识别不佳

    因为YOLOv1是最后直接学习框的值,这导致了它对异常长宽比的目标识别不佳。
  4. 对目标个数有限制

    由于YOLOv1最后的输出是固定的7x7,也就是说它最多只能预测49个目标。在我前面APP的截图中可以看到标注了max为100,这说明该版本的YOLO固定输出10x10。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:将检测问题变回归问题——YOLO

文章作者:高深远

发布时间:2020年01月30日 - 22:44

最后更新:2020年02月15日 - 22:06

原始链接:https://gsy00517.github.io/computer-vision20200130224438/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:将检测问题变回归问题——YOLO | 高深远的博客

computer vision笔记:将检测问题变回归问题——YOLO

在许多新的算法里,涉及到bounding box时,往往会转化成回归问题。而做出这种转变的开山之作,据我所知应该是YOLO。关于YOLO,网上有很多很好的解读,我也在之前的文章中多次提到过YOLO的一些要素,这里不一一列举了,也不重复了,可以使用我网站内的搜索功能看一下相关内容。Andrew Ng在卷积神经网络的公开课上好像也特地围绕YOLO的相关内容讲了一周吧,也可以去看看。本文主要是想记录一下YOLO设计损失函数的一点trick和一些理解。
注意,本文讨论的是YOLOv1。


损失函数

我们直接看YOLO的损失函数。

可以看到,这里所有的损失都是利用ground truth和预测作差。熟悉机器学习相关知识的读者应该会知道,一般我们计算分类的损失时,一般会使用交叉熵或者逻辑回归,而YOLO的类别损失(上图最后一项)依旧使用了直接作差的方法,所以我们说,YOLO把整个检测问题作为一个回归问题来处理。
暂时不关注其它损失项,我觉得这里比较巧妙的是第二项也就是宽高误差。善于观察的话肯定会发现,宽高误差在这里所有的损失项中略显突兀,即它在作差之前分别对ground truth和预测值开了根号,这是为什么呢?
实际上,YOLO的一个motivation就是解决密集物体和小物体的检测(虽然还是没能很好的解决)。相比于大的Bbox产生预测误差,作者更加关注小的Bbox产生的误差。而平方误差损失(sum-squared error loss)中对同样的偏移loss值是一样的。因此这里用了一种比较巧妙的办法,就是用长和宽的平方根来代替原值,这时小的Bbox发生偏移时,其横轴上反应的loss比大的Bbox发生偏移时要大。

这为我们提供了一种很好的思路,若我们对大跨度的目标更感兴趣或者说更希望较大的目标预测更精确时,我们不妨也可以使用平方或是立方来增强大尺度时的反应。其实我觉得这里的思想其实和Gamma校正是一致的。


YOLO优点

  1. 速度快且容易优化

    YOLO(You Only Look Once)只需读取一次图像就可以进行端到端的优化,这使得它的速度可以很快。它将检测问题转化为回归问题,可以满足实时性的要求。我手机上的一款APP用的就是YOLO的改进版。
  2. 背景误识别率低

    YOLO对全图进行卷积学习,综合考虑了全图的上下文信息,因此背景的误检测较少。
  3. 泛化能力强

    这也是因为综合考虑了图片全局,因此能够更好地学习数据集的本质表达,从而使得泛化性能更好。

YOLO缺点

  1. 定位精度不够

    这尤其是对于小目标,因为网络较深,使得细粒度特征和小物体特征不明显。因为它是端到端的检测算法(one-stage),没有Faster RCNN那样提取候选框的操作,这也导致了它的精度不高。

    补充:由于one-stage和two-stage检测都存在着速度与精度平衡的问题,所以后来的SSD(也是端到端)在借鉴YOLO的网络架构和Faster RCNN中的anchor box实现多尺度的思想的基础上,设计出了更好的算法。可以看一下computer-vision笔记:SSD和DSSD

  2. 对密集目标的识别存在不足

    这主要是因为一个grid cell只能预测一个物体。如下图所示,当两个bicycle靠得比较近时,只预测出了一个bicycle。

  3. 异常长宽比的目标识别不佳

    因为YOLOv1是最后直接学习框的值,这导致了它对异常长宽比的目标识别不佳。
  4. 对目标个数有限制

    由于YOLOv1最后的输出是固定的7x7,也就是说它最多只能预测49个目标。在我前面APP的截图中可以看到标注了max为100,这说明该版本的YOLO固定输出10x10。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:将检测问题变回归问题——YOLO

文章作者:高深远

发布时间:2020年01月30日 - 22:44

最后更新:2020年02月15日 - 22:06

原始链接:https://gsy00517.github.io/computer-vision20200130224438/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:SSD和DSSD | 高深远的博客

computer vision笔记:SSD和DSSD

最近看了目标检测中比较经典的SSD,觉得有不少挺好的创新。之前的文章中也提到过,由于one-stage的检测方法和two-stage的检测方法都存在着速度与精度平衡的问题,所以SSD在借鉴YOLO的网络架构和Faster RCNN中的anchor box实现多尺度的思想的基础上,设计出了效果更好的算法。而DSSD是SSD众多改进版本中比较突出的一支,主要是牺牲了fps来换取精度。本篇文章我就不花时间去写两个模型的流程了,主要是记录一些我觉得比较好的点。

References

电子文献:
https://zhuanlan.zhihu.com/p/33544892
https://www.cnblogs.com/edbean/p/11335139.html
https://www.zhihu.com/question/58200555


SSD和DSSD

背景

在SSD之前,目标检测主要有两种思路,一种是以YOLO为代表的基于一体化卷积网络的检测,即使用端到端的one-stage检测方法,其主要思路是均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,然后利用CNN提取特征后直接进行分类与回归,整个过程只需要一步,可参考deep-learning笔记:端到端学习;另一种思路是以RCNN系列为代表的先提取候选区域再进行分类与回归的two-stage检测方法,详见computer-vision笔记:RPN与Faster RCNN。前者的优势在速度,但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡,导致模型准确度稍低;后者的优势在精度,但总是无法取得平衡且更好的效果。

SSD

本文要介绍的SSD算法,其英文全名是Single Shot MultiBox Detector,其中Single shot指明了SSD算法属于one-stage方法,MultiBox指明了SSD使用了多框预测。

上图是几种方法的基本框架对比图,对于Faster RCNN,其先通过CNN得到候选框,然后再进行分类与回归,而YOLO与SSD可以一步到位完成检测。
相比YOLO,SSD采用CNN来直接进行检测,而不是像YOLO那样在全连接层之后做检测,这是SSD相比YOLO的其中一个不同点。另外还有两个重要的改变:一是SSD提取了不同尺度的特征图来做检测,大尺度特征图(较靠前的特征图)可以用来检测小物体,而小尺度特征图(较靠后的特征图)用来检测大物体(见下图);二是SSD采用了不同尺度和长宽比的先验框(Prior boxes,Default boxes,在Faster RCNN中叫做锚,Anchors)。YOLO算法缺点是难以检测小目标,而且定位不准,而上面这几点重要的改进使得SSD在一定程度上克服这些缺点。

DSSD

DSSD其实就是D加SSD。其中D代表反卷积,其重要意义就是可以提升分辨率,而提升分辨率的重要效果就是小物体检测性能提升;SSD代表其使用的backbone,这部分的与SSD的结构上无较大差异,主要是将VGG换为了更深的ResNet-101来获得更高的准确度,并在其后加入数个卷积层。单纯加卷积层并不能直接提升精度,但是在这之后加入后文提到的prediction module后就能极大地提升精度。
由于利用single-scale输出预测multi-scale物体在精度方面具有一定劣势,因此SSD利用各层的feature map的输出感受野各不相同的特性,用大的感受野检测大型物体,用小的感受野检测小型物体。但是若直接利用浅层的输出检测小型物体,由于浅层网络提取的语义信息较少(含有较多的背景和噪声),不够鲁棒,所以最终表现还不是很好。
这里我想介绍一下稍早于DSSD的FPN,它先还是一如既往地使用bottom-up的深度网络,然后又构建top-down网络进行上采样操作,这不仅使得它可以利用经过top-down模型后的那些上下文信息即高层语义信息,也可以在更大的feature map上面进行操作,提高了分辨率,可以获得更多关于小目标的有用信息。此外,不同于许多算法(SSD等)采用多尺度特征融合之后再做预测的方式,FPN的预测是在不同的特征层独立进行的。
于是DSSD从中得到启发,利用FPN的思想来改进SSD,它使用反卷积和skip connection扩大图像,这样除了能提升分辨率,还能保证语义信息充足。关于反卷积的实现,可参考computer-vision笔记:上采样和下采样
这样就形成了一个“宽-窄-宽”沙漏型的结构,其中网络的中间层用于编码(encode)输入图像的信息,然后再逐渐用更大的层来自上而下地解码(decode)在这整个图像上的图。要注意的是,这里的反卷积并不能复原原来的图像。类似的思想可以看一下我整理的computer-vision笔记:图像金字塔与高斯滤波器


空洞卷积(atrous convolutions)

在SSD中,作者采用了一种名为空洞卷积(atrous convolutions,又名扩张卷积(dilated convolutions))的卷积方式,向卷积层引入一个称为“扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时选取各值之间的间距。

空洞卷积的有效性基于一个假设,即紧密相邻的像素几乎相同,全部纳入会产生冗余,不如每隔H(hole size)个选取一个。
在相同的计算条件下,空洞卷积提供了更大的感受野。它经常用在实时图像分割中。当网络层需要较大的感受野,但计算资源有限而无法提高卷积核数量或大小时,可以考虑使用空洞卷积。

值得注意的是,空洞卷积在增大感受野的情况下也维持了分辨率(也就是说在相同感受野下分辨率更高),这在语义分割等问题中是比较实用的。此外,用步长为2的卷积操作代替池化也可以有效地减少分辨率的损失。


难例挖掘(hard negative mining)

首先介绍一下什么是难例。
根据IoU我们可以把采样获得的正负样本分成难易样本。
简单负样本:与GT没有任何交集。这种样本是最多的,在图像中随意采集往往都属于这一类。
简单正样本:与GT的交集远远大于阈值。也就是非常接近正确结果的。
困难负样本:与GT有交集,但小于阈值。
困难正样本:与GT有交集,且仅略大于阈值。
后面两种就是我们所说的难例,其loss较大。若在采样时难例占比太小,那么总体的loss就不大,这对训练来说是不利的。

因为本身正样本比较稀少,因此难例挖掘主要关注的是困难负样本,其基本策略是:

  1. 选取所有的正样本,数量记为k个。
  2. 对所有的负样本求loss,递减排序,选取前3k个。
  3. 用这k个正样本,3k个负样本参与损失计算与反向传播。(SSD和DSSD都采取了这样的比例)

smoothL1

在使用损失函数时,我们以往一般使用的是L1损失或者L2损失,而在Faster RCNN和SSD中都使用了smoothL1来作为损失函数。

为了更清楚地探究smoothL1的作用,我们将三者对x求导。

可以看到,对L1,其损失函数的导数随着x的增大而增大,这就导致了在训练初期,预测值与ground truth差异过于大时,损失函数对预测值的梯度十分大,训练不稳定。
对L2,其损失函数的导数为常数,这就导致了在训练后期,预测值与ground truth差异已经很小时,损失函数的导数的绝对值仍然为1,此时如果learning rate不变,那么模型参数将在稳定值附近波动,难以继续收敛以达到更高精度。一种思路是可以让学习率动态衰减,方法及实现详见deep-learning笔记:学习率衰减与批归一化
而smoothL1则从两个方面限制了梯度:
(1)当预测框与ground truth差别过大时,梯度值不至于过大。也就是避免了梯度爆炸,使得模型更加健壮。
(2)当预测框与ground truth差别很小时,梯度值足够小。
可以说smoothL1很好地解决了L1和L2的缺陷,且比较简便。


残差模块

在预测模块中,DSSD也对SSD进行了改进,在feature map之后还引入了残差模块来提升性能,如下图所示。

经比较可以发现,引入1个残差单元(即上方图c)的效果最好。


移去批归一化

在DSSD中,为了提高测试速度,作者还通过去掉BN层来提高模型的速度。
根据论文中的一组公式,我们可以简单地了解一下作者的方法。

实际上,作者的方法就是把批归一化进行变换拆分,从而将去掉的BN层加入到卷积层中。具体就是rewrite一个卷积层中如等式2所示的weight和如等式3所示的bias,从而就不需要等式4中相关的批归一化变量了。
作者通过实验也证明了这种方法对速度的提升(尽管还是很低)。


网络训练

因为在Fast RCNN和Faster RCNN中没有特征或者像素重新采样阶段,所以它们依赖于数据增强(data augmentation)。主要的方法有随机剪裁(random cropping),光度失真(random photometric distortion)和随机翻转(random flipping)。
在SSD中还包括了一种随机扩展(random expansion)的数据增强trick,这种trick对小检测对象比较有用。
DSSD在SSD的基础上进行训练,即先引入预训练得比较好的SSD,然后冻结SSD部分训练后面的部分,最后以较小的学习率训练整个网络来进行微调(fine-tuning)。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:SSD和DSSD

文章作者:高深远

发布时间:2020年02月01日 - 11:34

最后更新:2020年02月15日 - 22:08

原始链接:https://gsy00517.github.io/computer-vision20200201113416/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:SSD和DSSD | 高深远的博客

computer vision笔记:SSD和DSSD

最近看了目标检测中比较经典的SSD,觉得有不少挺好的创新。之前的文章中也提到过,由于one-stage的检测方法和two-stage的检测方法都存在着速度与精度平衡的问题,所以SSD在借鉴YOLO的网络架构和Faster RCNN中的anchor box实现多尺度的思想的基础上,设计出了效果更好的算法。而DSSD是SSD众多改进版本中比较突出的一支,主要是牺牲了fps来换取精度。本篇文章我就不花时间去写两个模型的流程了,主要是记录一些我觉得比较好的点。

References

电子文献:
https://zhuanlan.zhihu.com/p/33544892
https://www.cnblogs.com/edbean/p/11335139.html
https://www.zhihu.com/question/58200555


SSD和DSSD

背景

在SSD之前,目标检测主要有两种思路,一种是以YOLO为代表的基于一体化卷积网络的检测,即使用端到端的one-stage检测方法,其主要思路是均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,然后利用CNN提取特征后直接进行分类与回归,整个过程只需要一步,可参考deep-learning笔记:端到端学习;另一种思路是以RCNN系列为代表的先提取候选区域再进行分类与回归的two-stage检测方法,详见computer-vision笔记:RPN与Faster RCNN。前者的优势在速度,但是均匀的密集采样的一个重要缺点是训练比较困难,这主要是因为正样本与负样本(背景)极其不均衡,导致模型准确度稍低;后者的优势在精度,但总是无法取得平衡且更好的效果。

SSD

本文要介绍的SSD算法,其英文全名是Single Shot MultiBox Detector,其中Single shot指明了SSD算法属于one-stage方法,MultiBox指明了SSD使用了多框预测。

上图是几种方法的基本框架对比图,对于Faster RCNN,其先通过CNN得到候选框,然后再进行分类与回归,而YOLO与SSD可以一步到位完成检测。
相比YOLO,SSD采用CNN来直接进行检测,而不是像YOLO那样在全连接层之后做检测,这是SSD相比YOLO的其中一个不同点。另外还有两个重要的改变:一是SSD提取了不同尺度的特征图来做检测,大尺度特征图(较靠前的特征图)可以用来检测小物体,而小尺度特征图(较靠后的特征图)用来检测大物体(见下图);二是SSD采用了不同尺度和长宽比的先验框(Prior boxes,Default boxes,在Faster RCNN中叫做锚,Anchors)。YOLO算法缺点是难以检测小目标,而且定位不准,而上面这几点重要的改进使得SSD在一定程度上克服这些缺点。

DSSD

DSSD其实就是D加SSD。其中D代表反卷积,其重要意义就是可以提升分辨率,而提升分辨率的重要效果就是小物体检测性能提升;SSD代表其使用的backbone,这部分的与SSD的结构上无较大差异,主要是将VGG换为了更深的ResNet-101来获得更高的准确度,并在其后加入数个卷积层。单纯加卷积层并不能直接提升精度,但是在这之后加入后文提到的prediction module后就能极大地提升精度。
由于利用single-scale输出预测multi-scale物体在精度方面具有一定劣势,因此SSD利用各层的feature map的输出感受野各不相同的特性,用大的感受野检测大型物体,用小的感受野检测小型物体。但是若直接利用浅层的输出检测小型物体,由于浅层网络提取的语义信息较少(含有较多的背景和噪声),不够鲁棒,所以最终表现还不是很好。
这里我想介绍一下稍早于DSSD的FPN,它先还是一如既往地使用bottom-up的深度网络,然后又构建top-down网络进行上采样操作,这不仅使得它可以利用经过top-down模型后的那些上下文信息即高层语义信息,也可以在更大的feature map上面进行操作,提高了分辨率,可以获得更多关于小目标的有用信息。此外,不同于许多算法(SSD等)采用多尺度特征融合之后再做预测的方式,FPN的预测是在不同的特征层独立进行的。
于是DSSD从中得到启发,利用FPN的思想来改进SSD,它使用反卷积和skip connection扩大图像,这样除了能提升分辨率,还能保证语义信息充足。关于反卷积的实现,可参考computer-vision笔记:上采样和下采样
这样就形成了一个“宽-窄-宽”沙漏型的结构,其中网络的中间层用于编码(encode)输入图像的信息,然后再逐渐用更大的层来自上而下地解码(decode)在这整个图像上的图。要注意的是,这里的反卷积并不能复原原来的图像。类似的思想可以看一下我整理的computer-vision笔记:图像金字塔与高斯滤波器


空洞卷积(atrous convolutions)

在SSD中,作者采用了一种名为空洞卷积(atrous convolutions,又名扩张卷积(dilated convolutions))的卷积方式,向卷积层引入一个称为“扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时选取各值之间的间距。

空洞卷积的有效性基于一个假设,即紧密相邻的像素几乎相同,全部纳入会产生冗余,不如每隔H(hole size)个选取一个。
在相同的计算条件下,空洞卷积提供了更大的感受野。它经常用在实时图像分割中。当网络层需要较大的感受野,但计算资源有限而无法提高卷积核数量或大小时,可以考虑使用空洞卷积。

值得注意的是,空洞卷积在增大感受野的情况下也维持了分辨率(也就是说在相同感受野下分辨率更高),这在语义分割等问题中是比较实用的。此外,用步长为2的卷积操作代替池化也可以有效地减少分辨率的损失。


难例挖掘(hard negative mining)

首先介绍一下什么是难例。
根据IoU我们可以把采样获得的正负样本分成难易样本。
简单负样本:与GT没有任何交集。这种样本是最多的,在图像中随意采集往往都属于这一类。
简单正样本:与GT的交集远远大于阈值。也就是非常接近正确结果的。
困难负样本:与GT有交集,但小于阈值。
困难正样本:与GT有交集,且仅略大于阈值。
后面两种就是我们所说的难例,其loss较大。若在采样时难例占比太小,那么总体的loss就不大,这对训练来说是不利的。

因为本身正样本比较稀少,因此难例挖掘主要关注的是困难负样本,其基本策略是:

  1. 选取所有的正样本,数量记为k个。
  2. 对所有的负样本求loss,递减排序,选取前3k个。
  3. 用这k个正样本,3k个负样本参与损失计算与反向传播。(SSD和DSSD都采取了这样的比例)

smoothL1

在使用损失函数时,我们以往一般使用的是L1损失或者L2损失,而在Faster RCNN和SSD中都使用了smoothL1来作为损失函数。

为了更清楚地探究smoothL1的作用,我们将三者对x求导。

可以看到,对L1,其损失函数的导数随着x的增大而增大,这就导致了在训练初期,预测值与ground truth差异过于大时,损失函数对预测值的梯度十分大,训练不稳定。
对L2,其损失函数的导数为常数,这就导致了在训练后期,预测值与ground truth差异已经很小时,损失函数的导数的绝对值仍然为1,此时如果learning rate不变,那么模型参数将在稳定值附近波动,难以继续收敛以达到更高精度。一种思路是可以让学习率动态衰减,方法及实现详见deep-learning笔记:学习率衰减与批归一化
而smoothL1则从两个方面限制了梯度:
(1)当预测框与ground truth差别过大时,梯度值不至于过大。也就是避免了梯度爆炸,使得模型更加健壮。
(2)当预测框与ground truth差别很小时,梯度值足够小。
可以说smoothL1很好地解决了L1和L2的缺陷,且比较简便。


残差模块

在预测模块中,DSSD也对SSD进行了改进,在feature map之后还引入了残差模块来提升性能,如下图所示。

经比较可以发现,引入1个残差单元(即上方图c)的效果最好。


移去批归一化

在DSSD中,为了提高测试速度,作者还通过去掉BN层来提高模型的速度。
根据论文中的一组公式,我们可以简单地了解一下作者的方法。

实际上,作者的方法就是把批归一化进行变换拆分,从而将去掉的BN层加入到卷积层中。具体就是rewrite一个卷积层中如等式2所示的weight和如等式3所示的bias,从而就不需要等式4中相关的批归一化变量了。
作者通过实验也证明了这种方法对速度的提升(尽管还是很低)。


网络训练

因为在Fast RCNN和Faster RCNN中没有特征或者像素重新采样阶段,所以它们依赖于数据增强(data augmentation)。主要的方法有随机剪裁(random cropping),光度失真(random photometric distortion)和随机翻转(random flipping)。
在SSD中还包括了一种随机扩展(random expansion)的数据增强trick,这种trick对小检测对象比较有用。
DSSD在SSD的基础上进行训练,即先引入预训练得比较好的SSD,然后冻结SSD部分训练后面的部分,最后以较小的学习率训练整个网络来进行微调(fine-tuning)。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:SSD和DSSD

文章作者:高深远

发布时间:2020年02月01日 - 11:34

最后更新:2020年02月15日 - 22:08

原始链接:https://gsy00517.github.io/computer-vision20200201113416/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:上采样和下采样 | 高深远的博客

computer vision笔记:上采样和下采样

总是看到对图像进行上采样、对图像进行下采样,感觉好像懂了又不知道具体作了什么,这里就来搞搞懂。
本文针对的是图像处理中的上采样和下采样。

References

电子文献:
https://www.cnblogs.com/tectal/p/10138432.html
https://buptldy.github.io/2016/10/29/2016-10-29-deconv/


下采样

下采样即缩小图像,主要有两个目的:使得图像符合需要的大小;生成对应图像的缩略图。
下采样的原理很简单,比如对于一幅尺寸为MxN的图像,对其进行s倍下采样,即得到(M/s)x(N/s)尺寸的图像。这可以通过把原始图像划成sxs的窗口,使每个窗口内的图像变成一个像素,这个像素点的值可以是窗口内所有像素的均值或者最大值等等。另外我认为高斯滤波等卷积方式本身也是一种下采样。


上采样

上采样与下采样相反,其目的是放大图像。注意,它并不能带来更多关于该图像的信息,因此图像的质量会受到影响。
上采样的实现主要有两种思路。一种是内插值的方法,另一种是采用反卷积(也称转置卷积)的方法。

注:还有一种现在已经比较少用的方法也就是反池化,想了解的话可以看一下computer-vision笔记:反池化


插值方法

  1. 最邻近元法

    这种方法最简单,不需要计算,即在待求像素的四个邻像素中,选取距离待求像素最近的邻像素的灰度值赋给待求像素。 如上图所示,新增在A区内的像素就用左上角的像素点来赋值,其余三个区域同理。
    虽然最邻近元法计算量较小,但可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能会出现明显的锯齿状。
  2. 双线性插值法

    这种方法也很好理解,就是把原本四个像素点围成的新的区域中的灰度值变化视作线性的,然后根据插入点的位置,按照比例计算两次,得到最终要赋的值。 这也是一种比较常用的插值方法。
  3. 三次内插法

    该方法是理论上最优的sinc函数(辛格函数)逼近,其数学表达式为:$Sinc\left ( x \right )=\frac{sin\left ( x \right )}{x}$
    待求像素的灰度值由其周围16个灰度值加权内插得到,如下图所示。 待求像素的灰度计算式如下:其中$ABC$的含义分别如下所示。 三次内插法计算量比较大,但插值后的图像效果最好。

反卷积

反卷积通常用于如下几个方面:在CNN可视化处理中,可以通过反卷积将卷积得到的feature map还原到像素空间,来观察feature map对哪些pattern相应最大,即可视化哪些特征是卷积操作提取出来的;在FCN全卷积网络中,由于要对图像进行像素级的分割,需要将图像尺寸还原到原来的大小,类似upsampling的操作,所以需要采用反卷积;在GAN对抗式生成网络中,由于需要从输入图像到生成图像,自然需要将提取的特征图还原到和原图同样尺寸的大小,即也需要反卷积操作。
为了方便理解和比较,我们将卷积和反卷积对照着看。
我们先来看卷积时使用3x3卷积核,且没有padding,strides为1时的情况。


可以看到,由于卷积使用了no padding,那么对应的反卷积就要添加padding,反卷积的padding值可用padding = kernel_size - stride来计算。在上面的情况中,反卷积的padding就是3减1等于2。
前面说到反卷积也称转置卷积,这里说一下原因。
对于上面3x3卷积核的计算,我们可以通过这样一个4x16的稀疏矩阵来把卷积变成矩阵相乘$Y=CX$。

注意到这里每一行有9个数值和7个0,也就是说在上面4x4的输入特征中,对每一个输出特征,有9个输入特征参与了计算。
如此,卷积的前向操作可以表示为输入和稀疏矩阵$C$相乘。类似的,卷积的反向传播就是输出和$C$的转置相乘。

注意:熟悉线性代数的读者应该会发现,要实现上面的运算,不应该是求矩阵的逆吗。信息论告诉我们,卷积是不可逆的,因此要注意,上面的两个$C$指的并不是同一个,即用来进行反卷积的权重矩阵不一定来自于原卷积矩阵,但该权重矩阵的形状和转置后的原卷积矩阵完全相同。
此外,在一些深度学习的开源框架中,并不是通过这种转换方法来计算卷积的,因为这个转换会存在很多无用的0乘操作。

还有一个要注意的是,当卷积的stride为2时,要对输入矩阵中间补零才能完成反卷积,其实相当于进行了两次padding,一次内插补零,一次在外圈补零。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:上采样和下采样

文章作者:高深远

发布时间:2020年02月02日 - 13:32

最后更新:2020年02月07日 - 12:27

原始链接:https://gsy00517.github.io/computer-vision20200202133216/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:上采样和下采样 | 高深远的博客

computer vision笔记:上采样和下采样

总是看到对图像进行上采样、对图像进行下采样,感觉好像懂了又不知道具体作了什么,这里就来搞搞懂。
本文针对的是图像处理中的上采样和下采样。

References

电子文献:
https://www.cnblogs.com/tectal/p/10138432.html
https://buptldy.github.io/2016/10/29/2016-10-29-deconv/


下采样

下采样即缩小图像,主要有两个目的:使得图像符合需要的大小;生成对应图像的缩略图。
下采样的原理很简单,比如对于一幅尺寸为MxN的图像,对其进行s倍下采样,即得到(M/s)x(N/s)尺寸的图像。这可以通过把原始图像划成sxs的窗口,使每个窗口内的图像变成一个像素,这个像素点的值可以是窗口内所有像素的均值或者最大值等等。另外我认为高斯滤波等卷积方式本身也是一种下采样。


上采样

上采样与下采样相反,其目的是放大图像。注意,它并不能带来更多关于该图像的信息,因此图像的质量会受到影响。
上采样的实现主要有两种思路。一种是内插值的方法,另一种是采用反卷积(也称转置卷积)的方法。

注:还有一种现在已经比较少用的方法也就是反池化,想了解的话可以看一下computer-vision笔记:反池化


插值方法

  1. 最邻近元法

    这种方法最简单,不需要计算,即在待求像素的四个邻像素中,选取距离待求像素最近的邻像素的灰度值赋给待求像素。 如上图所示,新增在A区内的像素就用左上角的像素点来赋值,其余三个区域同理。
    虽然最邻近元法计算量较小,但可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能会出现明显的锯齿状。
  2. 双线性插值法

    这种方法也很好理解,就是把原本四个像素点围成的新的区域中的灰度值变化视作线性的,然后根据插入点的位置,按照比例计算两次,得到最终要赋的值。 这也是一种比较常用的插值方法。
  3. 三次内插法

    该方法是理论上最优的sinc函数(辛格函数)逼近,其数学表达式为:$Sinc\left ( x \right )=\frac{sin\left ( x \right )}{x}$
    待求像素的灰度值由其周围16个灰度值加权内插得到,如下图所示。 待求像素的灰度计算式如下:其中$ABC$的含义分别如下所示。 三次内插法计算量比较大,但插值后的图像效果最好。

反卷积

反卷积通常用于如下几个方面:在CNN可视化处理中,可以通过反卷积将卷积得到的feature map还原到像素空间,来观察feature map对哪些pattern相应最大,即可视化哪些特征是卷积操作提取出来的;在FCN全卷积网络中,由于要对图像进行像素级的分割,需要将图像尺寸还原到原来的大小,类似upsampling的操作,所以需要采用反卷积;在GAN对抗式生成网络中,由于需要从输入图像到生成图像,自然需要将提取的特征图还原到和原图同样尺寸的大小,即也需要反卷积操作。
为了方便理解和比较,我们将卷积和反卷积对照着看。
我们先来看卷积时使用3x3卷积核,且没有padding,strides为1时的情况。


可以看到,由于卷积使用了no padding,那么对应的反卷积就要添加padding,反卷积的padding值可用padding = kernel_size - stride来计算。在上面的情况中,反卷积的padding就是3减1等于2。
前面说到反卷积也称转置卷积,这里说一下原因。
对于上面3x3卷积核的计算,我们可以通过这样一个4x16的稀疏矩阵来把卷积变成矩阵相乘$Y=CX$。

注意到这里每一行有9个数值和7个0,也就是说在上面4x4的输入特征中,对每一个输出特征,有9个输入特征参与了计算。
如此,卷积的前向操作可以表示为输入和稀疏矩阵$C$相乘。类似的,卷积的反向传播就是输出和$C$的转置相乘。

注意:熟悉线性代数的读者应该会发现,要实现上面的运算,不应该是求矩阵的逆吗。信息论告诉我们,卷积是不可逆的,因此要注意,上面的两个$C$指的并不是同一个,即用来进行反卷积的权重矩阵不一定来自于原卷积矩阵,但该权重矩阵的形状和转置后的原卷积矩阵完全相同。
此外,在一些深度学习的开源框架中,并不是通过这种转换方法来计算卷积的,因为这个转换会存在很多无用的0乘操作。

还有一个要注意的是,当卷积的stride为2时,要对输入矩阵中间补零才能完成反卷积,其实相当于进行了两次padding,一次内插补零,一次在外圈补零。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:上采样和下采样

文章作者:高深远

发布时间:2020年02月02日 - 13:32

最后更新:2020年02月07日 - 12:27

原始链接:https://gsy00517.github.io/computer-vision20200202133216/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:元学习运用于目标跟踪——Meta-Tracker | 高深远的博客

computer vision笔记:元学习运用于目标跟踪——Meta-Tracker

之前在看元学习时,就觉得其中的一些想法很适合应用在目标跟踪领域。写完deep-learning笔记:元学习之后我就迫不及待地找了找有没有相关的论文。没想到还真有。这是一篇2018年的paper,我通过谷歌学术查了一下,发现被引次数有五六十次,觉得还是值得阅读一下的。

References

参考文献:
[1]Meta-Tracker: Fast and Robust Online Adaptation for Visual Object Trackers


主要思想

本篇论文主要将Meta Learning运用在了目标模型的初始化上。
首先作者认为结合深度特征和在线学习的模型有两大困难,一是训练的样本不容易获得,二是大多数state-of-the-art的tracker在训练阶段都需要花费大量的时间在初始化上面。
针对上面的难题,作者提出了一种在未来的帧上训练目标模型的思路。这样做有三点好处,一是能使模型更加关注对后续的帧更有价值的特征,二是避免了在当前帧上过拟合,三是能够使初始化更快速。总而言之,就是能保证精度和鲁棒性。
下图是作者运用元学习来初始化跟踪模型的流程,大概有个印象,后面会具体讲算法。

实验结果证明,这种方法能让模型在第一帧初始化是就能很快地提高精度和鲁棒性。(只需迭代一步)

补充:这篇paper多次出现“w.r.t”这个缩写,查了之后才知道就是“with respect to”的意思,也就是“关于,对于”的意思。更多文献常用缩写,见paper笔记:文献中各种缩写汇总


算法

下面直接来看算法,作者直接给了伪代码,挺好懂的。这里的$x$指的是输入,$\theta $指的是模型中的参数,$\alpha $指的是模型梯度下降更新时所用的学习率,$y$指的是我们预测值,而$\widehat{y}$指的是ground truth值。

算法的目的是找出符合元学习目标的合适的$\theta _{0}$和$\alpha $,文中加了星号表示。(对元学习比较模糊的话可以看看我前一篇文章)
算法可以分成两步:第一步使用随机初始化的$\theta _{0}$​和训练样本第一帧的图像进入网络得到一个输出$F$,然后根据ground truth得到loss,再引入一个$\alpha $作为梯度下降的动量参数,反复迭代T次后得到$\theta _{0}^{T}$​作为$\theta _{1}$;接下来第二步,就是看当前得到的参数是否对后面帧(每次迭代随机取一帧)的目标鲁棒,这里用loss对$\theta _{1}$​和$\alpha $的偏导数作为训练的梯度,对N个训练样本得到的梯度求和,用ADAM算法进行优化得到最优的初始参数$\theta _{0}$和$\alpha $。

注意:这里的$\theta _{0}^{T}$不是转置的意思,而是迭代第T次后的$\theta _{0}$。


缺点

善于承认不足是一种良好的品质,作者在文中也来了这样一个转折:“However, it often diverges on longer sequences or the sequences that have very small frame-to-frame variations.”意思是说这种方法在长序列或者目标在帧与帧之间变化不大时表现不佳(会偏离目标)。
回顾一下,当初设计这种方法的目的是模型在接收第一帧之后能够很快地收敛到精度和鲁棒性很好的一组参数位置,这就是说,我们训练得到的$\alpha $相对来说是比较大的,这就导致了它在上述情况下的不稳定。
为此,作者的解决办法是“find a learning rate for subsequent frames and then use existing optimization algorithms to update the models as was done in the original versions of the trackers”,也就是仅用学习到的$\theta _{0}$和$\alpha $来做初始化,然后在随后的在线更新过程中,仍用原来版本的方式进行更新。这里原来的版本指的是CREST和MDNet,作者在这两个tracker的基础上改进出了MetaCREST和MetaSDNet,下面接着讲。


改进CREST

CREST

CREST是一个代表性的结合深度特征使用相关滤波的tracker,它把相关滤波器转换成了一个卷积层,这使得它可以方便地添加新的模块,因为端到端模型的最优化可以用标准的反向传播梯度下降来完成。CREST还引入了时空域的残差模块来避免当物体发生较大变化时目标模型被过分地削弱。

问题

当然,要把作者提出的方法融合到其它的tracker中不是那么轻而易举的,主要有两个问题。

  1. PCA处理与元学习的矛盾

    CREST使用PCA来减少提出的CNN特征的通道数,从512维降到了64维。但是PCA对每一个序列做变换时都会改变基(basis),而元学习希望的是得到一种对每个序列都global的初始化方式,这就很矛盾了。
    于是作者这里用1x1的卷积层替代了CREST中原本的PCA处理,功能还是降维。
    由此一来,要学习的最优初始参数$\theta _{0}$就包含了以下两个:
    1. 降维的参数$\theta _{0_{d}}$。
    2. 相关滤波器的参数$\theta _{0_{f}}$。
  2. 滤波器与元学习的矛盾

    由于滤波器的尺寸大小根据目标的形状和大小一直在变化,而元学习需要的是固定的尺寸。考虑到对目标图像的强行变形会导致效果的严重减弱,作者采用了一种名为canonical size initialization的方法,也就是设置尺寸和比例为训练数据集的平均值以尽可能地减小对最终效果的影响。

MetaCREST

最终改进之后的MetaCREST框架如下图所示。

可以看一下它与原tracker的效果对比。

可以看到还是有一定进步的。


改进MDNet

MDNet

MDNet的特点就是在训练阶段使用了multi-domain的训练方式,这是它提升鲁棒性的关键。此外,它还使用了dropout和不同层之间不同的学习率来防止过拟合。

MetaSDNet

然而,作者用元学习把上述的multi-domain训练方式和正则化技术都替代了,仅用元学习来完成可以快速适应且鲁棒的分类器。

作者随后给出了两者的效果比较,可是好像并没有看到任何的明显的进步,两者效果几乎一样。

其实文章之所以强调MDNet是因为它初始化太慢了,在效果相同的情况下,MetaSDNet的初始化速度在MDNet的基础上加快了大约30倍。此外,在没有使用multi-domain训练方式和正则化技术的情况下,用元学习来达到相近的效果,也不能不说元学习的能力还是值得肯定的。


感想

自己接触元学习的相关算法之后,总想着直接套用到别的任务上去。这篇论文和我的想法有点类似,但作者在实际实现时肯定用了更多的方法来解决各种各样的难题。而这篇文章也只能说是用元学习的思想做了一些探索,引用作者的一部分总结:
“Other than target appearance modeling, which is the focus of this paper, there are many other important factors in object tracking algorithms. For example, when or how often to update the model, how to manage the database, and how to define the search space. These considerations are sometimes more important than target appearance modeling. In future work we propose including handling of these as part of learning and meta-learning.”
总而言之,还有很长的路要走~


碰到底线咯 后面没有啦

本文标题:computer vision笔记:元学习运用于目标跟踪——Meta-Tracker

文章作者:高深远

发布时间:2020年02月04日 - 21:03

最后更新:2020年02月15日 - 22:07

原始链接:https://gsy00517.github.io/computer-vision20200204210305/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:元学习运用于目标跟踪——Meta-Tracker | 高深远的博客

computer vision笔记:元学习运用于目标跟踪——Meta-Tracker

之前在看元学习时,就觉得其中的一些想法很适合应用在目标跟踪领域。写完deep-learning笔记:元学习之后我就迫不及待地找了找有没有相关的论文。没想到还真有。这是一篇2018年的paper,我通过谷歌学术查了一下,发现被引次数有五六十次,觉得还是值得阅读一下的。

References

参考文献:
[1]Meta-Tracker: Fast and Robust Online Adaptation for Visual Object Trackers


主要思想

本篇论文主要将Meta Learning运用在了目标模型的初始化上。
首先作者认为结合深度特征和在线学习的模型有两大困难,一是训练的样本不容易获得,二是大多数state-of-the-art的tracker在训练阶段都需要花费大量的时间在初始化上面。
针对上面的难题,作者提出了一种在未来的帧上训练目标模型的思路。这样做有三点好处,一是能使模型更加关注对后续的帧更有价值的特征,二是避免了在当前帧上过拟合,三是能够使初始化更快速。总而言之,就是能保证精度和鲁棒性。
下图是作者运用元学习来初始化跟踪模型的流程,大概有个印象,后面会具体讲算法。

实验结果证明,这种方法能让模型在第一帧初始化是就能很快地提高精度和鲁棒性。(只需迭代一步)

补充:这篇paper多次出现“w.r.t”这个缩写,查了之后才知道就是“with respect to”的意思,也就是“关于,对于”的意思。更多文献常用缩写,见paper笔记:文献中各种缩写汇总


算法

下面直接来看算法,作者直接给了伪代码,挺好懂的。这里的$x$指的是输入,$\theta $指的是模型中的参数,$\alpha $指的是模型梯度下降更新时所用的学习率,$y$指的是我们预测值,而$\widehat{y}$指的是ground truth值。

算法的目的是找出符合元学习目标的合适的$\theta _{0}$和$\alpha $,文中加了星号表示。(对元学习比较模糊的话可以看看我前一篇文章)
算法可以分成两步:第一步使用随机初始化的$\theta _{0}$​和训练样本第一帧的图像进入网络得到一个输出$F$,然后根据ground truth得到loss,再引入一个$\alpha $作为梯度下降的动量参数,反复迭代T次后得到$\theta _{0}^{T}$​作为$\theta _{1}$;接下来第二步,就是看当前得到的参数是否对后面帧(每次迭代随机取一帧)的目标鲁棒,这里用loss对$\theta _{1}$​和$\alpha $的偏导数作为训练的梯度,对N个训练样本得到的梯度求和,用ADAM算法进行优化得到最优的初始参数$\theta _{0}$和$\alpha $。

注意:这里的$\theta _{0}^{T}$不是转置的意思,而是迭代第T次后的$\theta _{0}$。


缺点

善于承认不足是一种良好的品质,作者在文中也来了这样一个转折:“However, it often diverges on longer sequences or the sequences that have very small frame-to-frame variations.”意思是说这种方法在长序列或者目标在帧与帧之间变化不大时表现不佳(会偏离目标)。
回顾一下,当初设计这种方法的目的是模型在接收第一帧之后能够很快地收敛到精度和鲁棒性很好的一组参数位置,这就是说,我们训练得到的$\alpha $相对来说是比较大的,这就导致了它在上述情况下的不稳定。
为此,作者的解决办法是“find a learning rate for subsequent frames and then use existing optimization algorithms to update the models as was done in the original versions of the trackers”,也就是仅用学习到的$\theta _{0}$和$\alpha $来做初始化,然后在随后的在线更新过程中,仍用原来版本的方式进行更新。这里原来的版本指的是CREST和MDNet,作者在这两个tracker的基础上改进出了MetaCREST和MetaSDNet,下面接着讲。


改进CREST

CREST

CREST是一个代表性的结合深度特征使用相关滤波的tracker,它把相关滤波器转换成了一个卷积层,这使得它可以方便地添加新的模块,因为端到端模型的最优化可以用标准的反向传播梯度下降来完成。CREST还引入了时空域的残差模块来避免当物体发生较大变化时目标模型被过分地削弱。

问题

当然,要把作者提出的方法融合到其它的tracker中不是那么轻而易举的,主要有两个问题。

  1. PCA处理与元学习的矛盾

    CREST使用PCA来减少提出的CNN特征的通道数,从512维降到了64维。但是PCA对每一个序列做变换时都会改变基(basis),而元学习希望的是得到一种对每个序列都global的初始化方式,这就很矛盾了。
    于是作者这里用1x1的卷积层替代了CREST中原本的PCA处理,功能还是降维。
    由此一来,要学习的最优初始参数$\theta _{0}$就包含了以下两个:
    1. 降维的参数$\theta _{0_{d}}$。
    2. 相关滤波器的参数$\theta _{0_{f}}$。
  2. 滤波器与元学习的矛盾

    由于滤波器的尺寸大小根据目标的形状和大小一直在变化,而元学习需要的是固定的尺寸。考虑到对目标图像的强行变形会导致效果的严重减弱,作者采用了一种名为canonical size initialization的方法,也就是设置尺寸和比例为训练数据集的平均值以尽可能地减小对最终效果的影响。

MetaCREST

最终改进之后的MetaCREST框架如下图所示。

可以看一下它与原tracker的效果对比。

可以看到还是有一定进步的。


改进MDNet

MDNet

MDNet的特点就是在训练阶段使用了multi-domain的训练方式,这是它提升鲁棒性的关键。此外,它还使用了dropout和不同层之间不同的学习率来防止过拟合。

MetaSDNet

然而,作者用元学习把上述的multi-domain训练方式和正则化技术都替代了,仅用元学习来完成可以快速适应且鲁棒的分类器。

作者随后给出了两者的效果比较,可是好像并没有看到任何的明显的进步,两者效果几乎一样。

其实文章之所以强调MDNet是因为它初始化太慢了,在效果相同的情况下,MetaSDNet的初始化速度在MDNet的基础上加快了大约30倍。此外,在没有使用multi-domain训练方式和正则化技术的情况下,用元学习来达到相近的效果,也不能不说元学习的能力还是值得肯定的。


感想

自己接触元学习的相关算法之后,总想着直接套用到别的任务上去。这篇论文和我的想法有点类似,但作者在实际实现时肯定用了更多的方法来解决各种各样的难题。而这篇文章也只能说是用元学习的思想做了一些探索,引用作者的一部分总结:
“Other than target appearance modeling, which is the focus of this paper, there are many other important factors in object tracking algorithms. For example, when or how often to update the model, how to manage the database, and how to define the search space. These considerations are sometimes more important than target appearance modeling. In future work we propose including handling of these as part of learning and meta-learning.”
总而言之,还有很长的路要走~


碰到底线咯 后面没有啦

本文标题:computer vision笔记:元学习运用于目标跟踪——Meta-Tracker

文章作者:高深远

发布时间:2020年02月04日 - 21:03

最后更新:2020年02月15日 - 22:07

原始链接:https://gsy00517.github.io/computer-vision20200204210305/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:反池化 | 高深远的博客

computer vision笔记:反池化

除了内插值和反卷积(详见computer-vision笔记:上采样和下采样),反池化也是一种上采样方法。由于近些年来有用卷积替代池化的趋势,因此已经比较少用了,但我还是想写一下这种方法和一些自己的思考。


反池化

上图是池化的一个示例,但由于后面要进行反池化,因此我们还需额外把(编码器中的)最大池化层的索引都存储起来,用于之后(解码器中的)的反池化操作。

之后反池化就很简单了,如下图所示,恢复到原始尺寸,将数据映射到索引处,其它地方填充零。


思考

当我刚看到反池化的操作方法时,有点感觉这样在扩充的空间中直接填充零会不会太草率了,为什么不用同一区域做相同的填充或者平均填充呢?
后来想了一想,的确还是直接将最大值映射到原位置,其余地方填充零效果最好。这样尽管会忽略邻近的信息,也就是会丢弃大量低频信息,但是它有助于保持高频信息的完整性。
反过来想,倘若我们将最大值相同地填充至一个区域内(上图中邻近的4格)或者做平均填充,那么不但不能准确地表示高频信息,又不能保证能够表达出低频信息,这就不如上面的做法能保证高频信息的完整性。


池化优点

虽然前面说池化正在逐渐被卷积替代,但池化操作也不是一无是处。与卷积相比,池化有如下优点:

  1. 减少了进行端到端训练的参数量。
  2. 对max-pooling来说,它可以提高边界的勾画,即突出高频信息。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:反池化

文章作者:高深远

发布时间:2020年02月07日 - 12:24

最后更新:2020年02月07日 - 13:05

原始链接:https://gsy00517.github.io/computer-vision20200207122427/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:反池化 | 高深远的博客

computer vision笔记:反池化

除了内插值和反卷积(详见computer-vision笔记:上采样和下采样),反池化也是一种上采样方法。由于近些年来有用卷积替代池化的趋势,因此已经比较少用了,但我还是想写一下这种方法和一些自己的思考。


反池化

上图是池化的一个示例,但由于后面要进行反池化,因此我们还需额外把(编码器中的)最大池化层的索引都存储起来,用于之后(解码器中的)的反池化操作。

之后反池化就很简单了,如下图所示,恢复到原始尺寸,将数据映射到索引处,其它地方填充零。


思考

当我刚看到反池化的操作方法时,有点感觉这样在扩充的空间中直接填充零会不会太草率了,为什么不用同一区域做相同的填充或者平均填充呢?
后来想了一想,的确还是直接将最大值映射到原位置,其余地方填充零效果最好。这样尽管会忽略邻近的信息,也就是会丢弃大量低频信息,但是它有助于保持高频信息的完整性。
反过来想,倘若我们将最大值相同地填充至一个区域内(上图中邻近的4格)或者做平均填充,那么不但不能准确地表示高频信息,又不能保证能够表达出低频信息,这就不如上面的做法能保证高频信息的完整性。


池化优点

虽然前面说池化正在逐渐被卷积替代,但池化操作也不是一无是处。与卷积相比,池化有如下优点:

  1. 减少了进行端到端训练的参数量。
  2. 对max-pooling来说,它可以提高边界的勾画,即突出高频信息。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:反池化

文章作者:高深远

发布时间:2020年02月07日 - 12:24

最后更新:2020年02月07日 - 13:05

原始链接:https://gsy00517.github.io/computer-vision20200207122427/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:IoU与GIoU | 高深远的博客

computer vision笔记:IoU与GIoU

之前在computer-vision笔记:non-max-suppression中已经介绍过IoU。而在2019年又提出了一种新的损失函数计算方式——GIoU,这里就简述一下其motivation和方法。

References

电子文献:
https://zhuanlan.zhihu.com/p/94799295


IoU的问题

  1. IoU与常用的边界框回归损失(smoothL1、均方误差MSE)没有强相关性,即损失相同时,IoU可能会有很大不同。
  2. 如果两个对象不重叠,则IoU值将为零,可此时就无法反映两个边界框彼此之间的距离。如下图所示,绿框与红框、绿框与蓝框的IoU都是零,可显然蓝框与红框的距离是比绿框要大得多的,这里用IoU就无法体现。
  3. IoU还有一个问题就是它无法正确区分两个对象的对齐方式,如下图所示,虽然它们的IoU是相同的,可是对齐方式大不相同,这点IoU也无法体现。

GIoU

针对IoU上述问题,GIoU巧妙地改进了定义的方法,提出了一种更强大的方式。简单来说可分为如下三步:

  1. 寻找两个边界框的最小闭包区域

    如下图所示,寻找A、B两个边界框的最小闭包区域C,一般就是最小外接凸多边形或者圆形。
  2. 计算IoU

    还是用相同的方法,现计算得出IoU。
  3. 计算GIoU

    我们可用如下公式计算GIoU的值用文字来描述,即先计算两个框的最小闭包区域面积,再计算闭包区域中不属于两个框的区域占闭包区域的比重,最后用IoU减去这个比重就得到GIoU。
    类似IoU的损失函数$L_{IoU}=1-IoU$,GIoU的损失函数计算公式如下

效果

实验表明,只需要将边界回归分支的损失修改为GIoU Loss,检测性能可以提升2%-14%,可以说非常引人瞩目了。
在文首给出的参考链接中,也介绍了一种名为DIoU的改进方式,使收敛更加快速,回归更加稳定,这里就不做介绍了。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:IoU与GIoU

文章作者:高深远

发布时间:2020年02月13日 - 23:11

最后更新:2020年04月14日 - 23:07

原始链接:https://gsy00517.github.io/computer-vision20200213231143/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:IoU与GIoU | 高深远的博客

computer vision笔记:IoU与GIoU

之前在computer-vision笔记:non-max-suppression中已经介绍过IoU。而在2019年又提出了一种新的损失函数计算方式——GIoU,这里就简述一下其motivation和方法。

References

电子文献:
https://zhuanlan.zhihu.com/p/94799295


IoU的问题

  1. IoU与常用的边界框回归损失(smoothL1、均方误差MSE)没有强相关性,即损失相同时,IoU可能会有很大不同。
  2. 如果两个对象不重叠,则IoU值将为零,可此时就无法反映两个边界框彼此之间的距离。如下图所示,绿框与红框、绿框与蓝框的IoU都是零,可显然蓝框与红框的距离是比绿框要大得多的,这里用IoU就无法体现。
  3. IoU还有一个问题就是它无法正确区分两个对象的对齐方式,如下图所示,虽然它们的IoU是相同的,可是对齐方式大不相同,这点IoU也无法体现。

GIoU

针对IoU上述问题,GIoU巧妙地改进了定义的方法,提出了一种更强大的方式。简单来说可分为如下三步:

  1. 寻找两个边界框的最小闭包区域

    如下图所示,寻找A、B两个边界框的最小闭包区域C,一般就是最小外接凸多边形或者圆形。
  2. 计算IoU

    还是用相同的方法,现计算得出IoU。
  3. 计算GIoU

    我们可用如下公式计算GIoU的值用文字来描述,即先计算两个框的最小闭包区域面积,再计算闭包区域中不属于两个框的区域占闭包区域的比重,最后用IoU减去这个比重就得到GIoU。
    类似IoU的损失函数$L_{IoU}=1-IoU$,GIoU的损失函数计算公式如下

效果

实验表明,只需要将边界回归分支的损失修改为GIoU Loss,检测性能可以提升2%-14%,可以说非常引人瞩目了。
在文首给出的参考链接中,也介绍了一种名为DIoU的改进方式,使收敛更加快速,回归更加稳定,这里就不做介绍了。


碰到底线咯 后面没有啦

本文标题:computer vision笔记:IoU与GIoU

文章作者:高深远

发布时间:2020年02月13日 - 23:11

最后更新:2020年04月14日 - 23:07

原始链接:https://gsy00517.github.io/computer-vision20200213231143/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:语义跟踪——FCNT | 高深远的博客

computer vision笔记:语义跟踪——FCNT

FCNT其实是一个比较老的工作了,性能跟现在是没法比的,但其中的许多进步之处还是非常有价值且值得思考的。事先注明,FCNT中的“FCN”非语义分割中的FCN,FCNT这里的“FCN”指的是全卷积网络。

References

电子文献:
https://www.cnblogs.com/Terrypython/p/10636259.html

参考文献:
[1]Visual Tracking with Fully Convolutional Networks


关注点

不同于以往的工作把CNN看成一个黑盒而不关注不同层的表现,FCNT关注了不同层的功能,即发现:顶层的CNN layer编码了更多的关于语义特征的信息并且可以作为类别检测器;而底层的CNN layer关注了更多局部特征,这有助于将目标从目标中分离出来。这个发现在之后的许多工作中也得到了应用和体现。如下图所示,这里的a图表示的是ground truth,b图表示的是使用VGG的conv4-3,也就是第10层产生的热力图,c图是通过conv5-3也就是第13层产生的热力图。

可以看到,较低维的CNN layer(conv4-3)能够更精准地表示目标的细粒度信息,而较高维的CNN layer(conv5-3)热力图显示较模糊,但对同类别的人也做出了响应。这就是说,顶层缺少类内特征区分,对类间识别比较好,更适合作语义分割;底层则反之,能够更好地表达目标的类内特征和位置信息。
基于不同层(顶层和底层)之间提取特征的不同,作者提出了一种新的tracking方法,利用两种特征相互补充辅助,来处理剧烈的外观变化(顶层特征发挥的作用)和区分目标本身(底层特征发挥的作用)。


稀疏表示

为了简化学习任务,降低模型复杂度,作者采用了稀疏表示的方法。其实基本上等同于字典学习,详见machine-learning笔记:SVD与字典学习
简而言之,字典学习就是寻找一个稀疏矩阵和一个字典矩阵,使它们的乘积尽可能地接近原本的数据。其步骤一般是先求得一个尽可能稀疏的稀疏矩阵,然后固定该稀疏矩阵来更新字典。
在FCNT中,稀疏表示是这样进行的:

注:这里的$\pi$表示的是前景mask,$F$指的是feature map,$c$表示稀疏项。

由于稀疏项$c$已经足够稀疏,我们可以直接省去接近于零的$\lambda \left | c \right |_{1}$。

  1. 首先我们计算前景(也就是目标物体)的误差$e=\underset{i}{min}\left | \pi -Fc_{i} \right |_{2}^{2}$,这一步也就是判断框内有没有目标出现,当$e$小于阈值时,即认为存在目标物体。
  2. 然后计算的是目标物体属于哪一类,即求出使得误差最小的类别的ID,利用公式$id=arg\underset{i}{min}\left | \pi -Fc_{i} \right |_{2}^{2}$。注意这里用了$arg$。

实现

网络结构

由于feature map本身是有内在结构的,有很多的feature map对目标的表达其实并没有起到作用,因此作者设计了一种方法来自动选择高维或者低维CNN上的feature map,同时忽略另一个feature map和噪声。下面就来简单介绍一下该方法的具体步骤,首先来看看FCNT的网络结构。

具体步骤

结合上图,简单介绍一下FCNT的实现流程。

  1. 第一步

    根据给定的target,对VGG的conv4-3和conv5-3进行特征图选择,其目的是选出最相关的特征图。
  2. 第二步

    根据conv5-3的筛选,建立广义的通用网络GNet,用于捕捉目标的类别信息。
  3. 第三步

    根据conv4-3的筛选,建立具有针对性的特定网络SNet,用于将目标从背景中区分出来。
  4. 第四步

    利用第一帧图像来初始化GNet和SNet并进行热力图回归,但要注意两个网络采用不更新的方法。
  5. 第五步

    对于新一帧图像,我们在上一帧目标的位置搜寻RoI,抠取之后送入全卷积网络。
  6. 第六步

    GNet和SNet各自产生一个前景热力图,然后通过最后的干扰检测器选择策略决定使用哪个热力图来确定下一帧的目标位置。

特征图选择

第一步中的特征图选择可能会让人比较疑惑,其实这个选择模型仅用了一个dropout层和一个卷积层,其目标就是使得目标的mask和预测出来的目标热力图尽可能相近。这里把输入的特征图向量化,并用二阶泰勒展开表示特征图扰动,也就是特征图的变化(加入噪声)带来的损失的变化。为了高效地在反向传播中计算,作者仅保留了Hessian矩阵中的对角线上的内容,而忽略泰勒展开式中的其它导数项,也就是保留$h_{ii}$而忽略$h_{ij}$,这就在计算一阶导数和二阶导数是更加高效。

补充:其实mask我之前也是一直不懂的,这里做个补充。
mask中文翻译为“掩模”,但它的功能不仅仅局限于遮掩,总的来说,mask有下面四种用途:

  1. 提取感兴趣区:用预先制作的mask与待处理图像相乘,RoI内图像值保持不变,而区外图像值都变为0。
  2. 屏蔽:用mask对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
  3. 结构特征提取:用相似性变量或图像匹配方法检测和提取图像中与mask相似的结构特征。
  4. 特殊形状图像的制作:其实实现方法一样,只是目的不一。

目标定位

FCNT的定位过程分为两步。

  1. 默认使用GNet

    先把GNet输出的热力图作为目标候选。这是因为GNet使用的是顶层特征,能够更好地处理形变、旋转和遮挡等目标跟踪中的常见问题,举个例子,前面也提到了,顶层特征对画面中同样类别的个体都有反应,这就使得即使目标发生变化,但顶层较为丰富的语义表达依旧能把它判断出来(可能认为是同一类的)。
  2. 判断是否使用SNet

    考虑若画面中出现同类物体时GNet不能很好的处理,因此还需要计算有没有出现目标漂移的情况。其方法是计算在目标候选区域外出现相似目标的概率P,定义一个阈值,若P大于阈值时则认为出现了同类目标,这时候才利用SNet来定位目标的最终位置,是结果更加准确。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:语义跟踪——FCNT

文章作者:高深远

发布时间:2020年02月14日 - 22:23

最后更新:2020年02月15日 - 22:07

原始链接:https://gsy00517.github.io/computer-vision20200214222332/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:语义跟踪——FCNT | 高深远的博客

computer vision笔记:语义跟踪——FCNT

FCNT其实是一个比较老的工作了,性能跟现在是没法比的,但其中的许多进步之处还是非常有价值且值得思考的。事先注明,FCNT中的“FCN”非语义分割中的FCN,FCNT这里的“FCN”指的是全卷积网络。

References

电子文献:
https://www.cnblogs.com/Terrypython/p/10636259.html

参考文献:
[1]Visual Tracking with Fully Convolutional Networks


关注点

不同于以往的工作把CNN看成一个黑盒而不关注不同层的表现,FCNT关注了不同层的功能,即发现:顶层的CNN layer编码了更多的关于语义特征的信息并且可以作为类别检测器;而底层的CNN layer关注了更多局部特征,这有助于将目标从目标中分离出来。这个发现在之后的许多工作中也得到了应用和体现。如下图所示,这里的a图表示的是ground truth,b图表示的是使用VGG的conv4-3,也就是第10层产生的热力图,c图是通过conv5-3也就是第13层产生的热力图。

可以看到,较低维的CNN layer(conv4-3)能够更精准地表示目标的细粒度信息,而较高维的CNN layer(conv5-3)热力图显示较模糊,但对同类别的人也做出了响应。这就是说,顶层缺少类内特征区分,对类间识别比较好,更适合作语义分割;底层则反之,能够更好地表达目标的类内特征和位置信息。
基于不同层(顶层和底层)之间提取特征的不同,作者提出了一种新的tracking方法,利用两种特征相互补充辅助,来处理剧烈的外观变化(顶层特征发挥的作用)和区分目标本身(底层特征发挥的作用)。


稀疏表示

为了简化学习任务,降低模型复杂度,作者采用了稀疏表示的方法。其实基本上等同于字典学习,详见machine-learning笔记:SVD与字典学习
简而言之,字典学习就是寻找一个稀疏矩阵和一个字典矩阵,使它们的乘积尽可能地接近原本的数据。其步骤一般是先求得一个尽可能稀疏的稀疏矩阵,然后固定该稀疏矩阵来更新字典。
在FCNT中,稀疏表示是这样进行的:

注:这里的$\pi$表示的是前景mask,$F$指的是feature map,$c$表示稀疏项。

由于稀疏项$c$已经足够稀疏,我们可以直接省去接近于零的$\lambda \left | c \right |_{1}$。

  1. 首先我们计算前景(也就是目标物体)的误差$e=\underset{i}{min}\left | \pi -Fc_{i} \right |_{2}^{2}$,这一步也就是判断框内有没有目标出现,当$e$小于阈值时,即认为存在目标物体。
  2. 然后计算的是目标物体属于哪一类,即求出使得误差最小的类别的ID,利用公式$id=arg\underset{i}{min}\left | \pi -Fc_{i} \right |_{2}^{2}$。注意这里用了$arg$。

实现

网络结构

由于feature map本身是有内在结构的,有很多的feature map对目标的表达其实并没有起到作用,因此作者设计了一种方法来自动选择高维或者低维CNN上的feature map,同时忽略另一个feature map和噪声。下面就来简单介绍一下该方法的具体步骤,首先来看看FCNT的网络结构。

具体步骤

结合上图,简单介绍一下FCNT的实现流程。

  1. 第一步

    根据给定的target,对VGG的conv4-3和conv5-3进行特征图选择,其目的是选出最相关的特征图。
  2. 第二步

    根据conv5-3的筛选,建立广义的通用网络GNet,用于捕捉目标的类别信息。
  3. 第三步

    根据conv4-3的筛选,建立具有针对性的特定网络SNet,用于将目标从背景中区分出来。
  4. 第四步

    利用第一帧图像来初始化GNet和SNet并进行热力图回归,但要注意两个网络采用不更新的方法。
  5. 第五步

    对于新一帧图像,我们在上一帧目标的位置搜寻RoI,抠取之后送入全卷积网络。
  6. 第六步

    GNet和SNet各自产生一个前景热力图,然后通过最后的干扰检测器选择策略决定使用哪个热力图来确定下一帧的目标位置。

特征图选择

第一步中的特征图选择可能会让人比较疑惑,其实这个选择模型仅用了一个dropout层和一个卷积层,其目标就是使得目标的mask和预测出来的目标热力图尽可能相近。这里把输入的特征图向量化,并用二阶泰勒展开表示特征图扰动,也就是特征图的变化(加入噪声)带来的损失的变化。为了高效地在反向传播中计算,作者仅保留了Hessian矩阵中的对角线上的内容,而忽略泰勒展开式中的其它导数项,也就是保留$h_{ii}$而忽略$h_{ij}$,这就在计算一阶导数和二阶导数是更加高效。

补充:其实mask我之前也是一直不懂的,这里做个补充。
mask中文翻译为“掩模”,但它的功能不仅仅局限于遮掩,总的来说,mask有下面四种用途:

  1. 提取感兴趣区:用预先制作的mask与待处理图像相乘,RoI内图像值保持不变,而区外图像值都变为0。
  2. 屏蔽:用mask对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
  3. 结构特征提取:用相似性变量或图像匹配方法检测和提取图像中与mask相似的结构特征。
  4. 特殊形状图像的制作:其实实现方法一样,只是目的不一。

目标定位

FCNT的定位过程分为两步。

  1. 默认使用GNet

    先把GNet输出的热力图作为目标候选。这是因为GNet使用的是顶层特征,能够更好地处理形变、旋转和遮挡等目标跟踪中的常见问题,举个例子,前面也提到了,顶层特征对画面中同样类别的个体都有反应,这就使得即使目标发生变化,但顶层较为丰富的语义表达依旧能把它判断出来(可能认为是同一类的)。
  2. 判断是否使用SNet

    考虑若画面中出现同类物体时GNet不能很好的处理,因此还需要计算有没有出现目标漂移的情况。其方法是计算在目标候选区域外出现相似目标的概率P,定义一个阈值,若P大于阈值时则认为出现了同类目标,这时候才利用SNet来定位目标的最终位置,是结果更加准确。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:语义跟踪——FCNT

文章作者:高深远

发布时间:2020年02月14日 - 22:23

最后更新:2020年02月15日 - 22:07

原始链接:https://gsy00517.github.io/computer-vision20200214222332/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:目标跟踪的小总结 | 高深远的博客

computer vision笔记:目标跟踪的小总结

看了一寒假的目标跟踪,一直想将自己学习到的内容整理归纳一下,迟迟没动笔(其实是打字,但说迟迟没打字比较难听哈哈)。如今快要开学了,决定还是积累一下。
本文主要是把目标跟踪中的一些相关要点做一个总结,并且按具体问题对一些主流的或者我阅读过觉得有价值的算法做一个简单概述。近年来各类方法层出不穷,且码字不易,无法涵盖所有的方法。此外,由于包含自己的转述和理解可能会存在错误。在今后的学习过程中会一直保持本文的更新,因此这将是一篇LTS的文章哈哈。

注:本文重点关注单目标跟踪。

References

参考文献:
[1]统计学习方法(第2版)
[2]Understanding and Diagnosing Visual Tracking Systems
[3]Survey of Visual Object Tracking Algorithms Based on Deep Learning
[4]Handcrafted and Deep Trackers: Recent Visual Object Tracking Approaches and Trends
[5]Review of visual object tracking technology
[6]A Review of Visual Trackers and Analysis of its Application to Mobile Robot
[7]Deep Learning for Visual Tracking: A Comprehensive Survey
[8]Video Object Segmentation and Tracking: A Survey
[9]Object Tracking Benchmark
[10]The Visual Object Tracking VOT2013 challenge results
[11]The Visual Object Tracking VOT2014 challenge results
[12]The Visual Object Tracking VOT2015 challenge results
[13]The Visual Object Tracking VOT2016 challenge results
[14]The Visual Object Tracking VOT2017 challenge results
[15]The sixth Visual Object Tracking VOT2018 challenge results
[16]The Seventh Visual Object Tracking VOT2019 Challenge Results

注:参考文献重新整理中,待补全…


简介与要求

目标跟踪是利用一个视频或图像序列的上下文信息,对目标的外观和运动信息进行建模,从而对目标运动状态进行预测并标定目标位置的一种技术。一般是在第一帧给出一个框,框中的物体就是我们需要在后续帧中用算法进行跟踪的对象。就目前的单目标跟踪而言,一般有如下要求:
monocular:我们的视频或者图片序列是仅从一个摄像头中获得的,也就是不考虑比如在城市道路场景中跨摄像头对目标跟踪的复杂应用。
model-free:没有任何先验,也就是在获取第一帧的框之前我们并不知道会框出什么物体,也不需要在之前对初始框中的物体进行建模。
single-target:只追踪第一帧框出的那一个物体,也就是除了那个物体之外所有的物体都是back ground。
casual/real-time:目标跟踪是一个在线过程,也就是不能提前获取未来的框对目标进行跟踪。
short-term:没有重检测,也就是目标跟丢了就丢了。
long-term:可以在跟丢之后重检测,这类算法一般除了跟踪之外还需要有检测的功能。

下面是目标跟踪流程的伪代码表示(不一定普适,比如有些算法不在线更新,但符合基本的过程)。


问题及挑战

通俗来讲,目标跟踪的最终目标就是要又快又准。“快”主要表现在计算量小和所需的存储空间小,“准”就是预测出的bounding box要尽可能地接近ground truth。除了上面两个基本需求(也可以说是为了更好地达到这两个基本需求),近年来的算法主要针对目标跟踪中的一些挑战进行突破,从而更好地解决某些问题之后达到更好的整体效果。
总的来说,目标跟踪的主要问题有如下这些:遮挡(occlusion)、背景干扰(background clutter)、光照变化(illumination changes)、尺度变化(scale variation)、低分辨率(low resolution)、快速移动(fast motion)、超出画面(out of view)、运动模糊(motion blur)、形变(deformation)、旋转(rotation)等。

OTB数据集依据各种问题对其中的序列进行了一个划分,这对之后针对性的研究提供了重要的参考。


生成式与判别式

利用特征判断候选样本是否为跟踪目标,可将目标跟踪的模型分为生成式模型和判别式模型,本小节就介绍一下什么是生成式模型和判别式模型。

机器学习

我们首先看看在机器学习中生成式模型和判别式模型定义的一般区分。
一般而言,机器学习的任务就是学习一个模型,应用这一个模型,对给定的输入预测相应的输出。输出的一般形式可以是决策函数,也可以是条件概率分布。
对于生成式模型,我们需要通过数据学习输入X与输出Y之间的生成关系(比如联合概率分布),也就是认为X和Y都是随机变量。典型的生成式模型有朴素贝叶斯模型、隐马尔可夫模型(HMM)、高斯混合模型(GMM)等。
对于判别式模型,我们只需要直接学习决策函数或者条件概率分布,只关心对给定的输入X我们需要输出怎么样的Y,也就是不考虑X是否是随机变量。典型的判别式模型包括k近邻、感知机、决策树、逻辑斯蒂回归模型、最大熵模型、支持向量机(SVM)、提升方法和条件随机场等。此外神经网络也属于判别式模型。
相较而言,生成式模型体现了更多的信息,不过这还是因条件而异的,不同情况不同任务两种方法各有优缺点。

目标跟踪

在目标跟踪领域,生成式模型通过提取目标特征来构建表观模型,然后在图像中搜索与模型最匹配的区域作为跟踪结果。不论采用全局特征还是局部特征,生成式模型的本质是在目标表示的高维空间中,找到与目标模型最相邻的候选目标作为当前估计。此类方法的缺陷在于只关注目标信息,而忽略了背景信息。

与生成式模型不同的是,判别式模型同时考虑了目标和背景信息。它将跟踪问题看做二分类或者回归问题,其目的是寻找一个判别函数,将目标从背景中分离出来,从而实现对目标的跟踪。

一般来说,在目标跟踪领域,判别式充分利用了目标前景和背景信息,能更加有效地区分出目标,比单单运用目标区域特征进行模板匹配的生成式模型在复杂环境中的鲁棒性更强。


算法导图

首先是参考文献[6]中的一个树状导图。

下图是中科院博士王强(github名为foolwood…呃不得不说这名字取得真谦虚)在github上上总结的Benchmark Results中的一个思维导图,同一个链接下还包括了各项成果的paper及code,值得收藏一下。

补充:这里再推荐一个在github上维护的Tracking Benchmark for Correlation Filters,按每篇论文针对或者解决的问题来分类,比较清楚,可以收藏一下。但这个仓库似乎在2017年后就没有更新了,可能是深度学习的进入或者说相关滤波系列和深度学习融合使得独立的相关滤波算法不那么突出了。

下图是浙大硕士王蒙蒙极市平台做分享的时候所用的一张思维导图,归纳得也比较清晰。

注:后两张导图中都把历年benchmark的冠军工作作了标注。

对比几张思维导图可以发现,他们都把主流算法分成了相关滤波、深度学习两个分支(或者说是基于handcrafted特征的算法和基于CNN提取特征的算法,其实近年已有所融合),此外还有一些基于强化学习、结构化SVM的模型。其实,目标跟踪算得上是计算机视觉领域中深度学习涉足较晚的一个方向,其主要原因是目标跟踪相关数据集的标注花费较大。此外,相关滤波的速度优势,也就是实时性是十分引人注目的,但在应付当前目标跟踪中的各种挑战、问题时,相关滤波的鲁棒性还是落后于深度学习方法的。
在下一节,我将结合上面几张导图,对历年尤其是近几年的算法做一个简单的整理,以方便日后的学习与研究。


各类算法的梳理与简述

本节按年份顺序对各个算法进行一个简单地梳理,其中各个算法的年份以论文发表的年份或者参加benchmark的年份为依据,可能会存在1年的区别,但影响不大。其中各年的各个算法根据算法的效果和影响大致上呈递减排序。对2013以后的算法,我拷贝了VOT challenge的结果排名,以供参照。

注意:如果你对计算机视觉或者说目标跟踪方面的一些基础方法、概念和经典算法已经有些了解,可以跳过本条建议。
考虑到在后文频繁地插入链接不太好,我就在此先推荐一下我博客的几个标签目标跟踪计算机视觉深度学习机器学习以及线性代数,其中的文章包含了一部分接下来要提到的概念和算法,可以事先浏览一下。当你在阅读时对相关概念、方法感到迷惑或者想进一步了解,博客内置的搜索功能或许能够为你提供帮助。

1981

LK Tracker

LK Tracker应该是最早的目标跟踪工作,它使用了光流的概念,如下图所示,不同颜色表示光流不同的方向,颜色的深浅表示运动的速度。

LK Tracker假定目标灰度在短时间内保持不变,同时目标邻域内的速度向量场变化缓慢。由于光流方程包含坐标x,y和时间t共三个未知数,其中时间变化dt已知而坐标变化dx和dy未知,一个方程两个未知数无法求解,因此作者假定相邻的点它们的光流具有空间一致性,即实际场景中邻近的点投影到图像上也是邻近点,且邻近点速度一致,这样就可以求解方程组了。下图是求解之后的光流向量,其中绿色箭头的方向表示运动方向,线段长度表示运动速度的大小。

光流的计算非常简单也非常快,而且由于提出得很早,各种库都有实现好的轮子可以轻松调用,但是它的鲁棒性不好,基本上只能对平移且外观不变的物体进行跟踪。

1994

KLT

KLT是一种生成式方法,也是使用了光流特征。在此基础上,作者使用了匹配角点的方法,也就是寻找边角处、纹理处等易辨识的地方计算光流来进行追踪。

1998

Condensation

Condensation(Conditional density propagation)条件密度传播使用了原始的外观作为主要特征来描述目标,采用了粒子滤波,这是一种非参数化滤波方法,属于生成式模型。它定义了一个粒子样本集,该样本集描述了每个粒子的坐标、运动速度、高和宽、尺度变化等状态;此外,通过一个状态转移矩阵和噪声定义系统状态方程。基于蒙特卡洛方法,粒子滤波将贝叶斯滤波方法中的积分运算转化为粒子采样求样本均值问题,通过对状态空间的粒子的随机采样来近似求解后验概率。

2002

Mean Shift

Mean Shift采用均值漂移作为搜索策略,这是一种无参概率估计方法,该方法利用图像特征直方图构造空间平滑的概率密度函数,通过沿着概率密度函数的梯度方向迭代,搜索函数局部最大值。在当时成为了常用的视觉跟踪系统的目标搜索方法,简单易实现,但鲁棒性较低。

2003

Feature Selection

Feature Selection利用线性判别分析自适应地选择对当前背景和目标最具鉴别性的颜色特征,从而分离出目标。

2006

Boosting

Boosting结合Haar特征和在线Boosting算法对目标进行跟踪。Boosting算法的基本思路就是首先均匀地初始化训练集中各个样本的权重,然后初始化N个弱分类器,通过训练集进行训练。第一次训练时,对第一个弱分类器,通过它在训练集上的错误率确定它的权重,同时更新训练集的样本权重(增加分类错误的样本的权重),然后,用新的训练集训练第二个弱分类器,计算它的权重并更新训练集的权重。如此迭代,将得到的分类器与它们的权重相乘,累加起来便得到一个强分类器。
上面所述是针对离线训练的,当在线训练(比如跟踪)时,为了满足实时性,就必须减少样本数量。Boosting的做法是对每一帧采集的样本仅使用一次便丢弃,然后进入下一帧采用新的样本。
以上就是在线Boosting算法的简单理解,具体而言,Boosting这里选择的弱分类器其实是Haar特征。由于Haar特征其实是一组特征,于是就需要Boosting算法根据每种Haar特征的响应来从Haar特征池中选出一个子集用于构造强分类器。

2008

IVT

IVT渐进地学习一个低维的子空间表示来自适应目标物体的变化,它将以前检测到的目标乘以遗忘因子作为样本在线更新特征空间的基而无需大量的标注样本。

2010

MOSSE

MOSSE(Minimum Output Sum of Squared Error)使用相关滤波来做目标跟踪(不是第一个,但可以看作前期的一个代表),其速度能够达到600多帧每秒,但是效果一般,这主要是因为它只使用了简单的raw pixel特征。
相比之前的算法,MOSSE能够形成更加明确的峰值,减少了漂移;此外,MOSSE可以在线更新,同时还采用了PSR来检测遮挡或者跟丢的情况,从而决定是否需要停止更新。
值得一提的是,MOSSE在做相关操作之前,对每张图都进行了减去平均值的处理,这有利于淡化背景对相关操作的影响。另外假如发生光照变化的话,减去均值也有利于减小这种变化的影响。此外要注意,输出的特征应乘以汉宁窗(一种余弦窗),用于确定搜索区域(也就是不为0的区域),且有利于突出中心的特征。

TLD

TLD(Tracking Learning Detection)主要针对long-term tracking,在跟踪的同时全局检测。它由三部分组成:跟踪模块、检测模块、学习模块。
跟踪模块观察帧与帧之间的目标的动向。作者采用了光流来跟踪,此外还提出了一种判断跟踪失效的算法,由于光流跟踪时选取的若干特征点,当其中某一个特征点的位移与所有特征点位移的中值之差过大时,也就是某个特征点离跟踪模块认为的目标中心位置很远时,就认为跟踪失效。作者还通过相似度和错误匹配度来对特征点进行筛选。
检测模块把每张图看成独立的,然后对单张图片进行目标检测定位。作者使用了方差检测器、随机森林和最近邻分类器来对目标做检测。
学习模块对根据跟踪模块的结果对检测模块的错误进行评估,当置信度较低时,重新组织正负样本对随机深林的后验概率和最近邻分类器的在线模板进行更新,从而避免以后出现类似错误。
TLD与传统跟踪算法的显著区别在于将传统的跟踪算法和传统的检测算法相结合来解决被跟踪目标在被跟踪过程中发生的形变、部分遮挡等问题。同时,通过一种改进的在线学习机制不断更新跟踪模块的“显著特征点”和检测模块的目标模型及相关参数,从而使得跟踪能够自适应,效果较之前更加稳定、可靠。

2011

FoT

FoT(Flock of Trackers)首先在目标上抓取多个interesting point并分别放入多个cell中,之后的跟踪就是检测并补偿每个cell中interesting point的偏动量,使其回到中间。如果interesting point超出cell,则让它重新恢复到cell的中点。

此外,FoT还提出了两种简单有效的failure预测方法:neighbourhood consistency predictor(Nh)和Markov predictor(Mp)。Nh的基本思想是认为正确的跟踪情况下每个local tracker给出的位移应当与它相邻的tracker相一致,而Mp主要是针对时域一致性,认为前几帧表现较好的local tracker在当前帧也会有较好的表现。FoT基于这些failure预测方法来控制模型的更新。

Struck

Struck的主要贡献是引入了结构化SVM。考虑到传统的跟踪算法将跟踪问题转化为一个分类问题,并通过在线学习技术更新目标模型。然而,为了达到更新的目的,通常需要将一些预估计的目标位置作为已知类别的训练样本,这些分类样本并不一定与实际目标一致,因此难以实现最佳的分类效果。
结合上述考虑,Struck利用了结构化SVM直接输出跟踪结果,避免了中间分类环节,这使得在当时效果有明显的提升。同时,为了保证实时性,Struck还引入了阈值机制,防止跟踪过程中支持向量的过增长。

L1 Tracker

L1 Tracker是第一个将稀疏编码引入目标跟踪问题中的算法。它把跟踪看做一个稀疏近似问题,主要是用第一帧和最近几帧得到的图像(特征)作为字典,通过求解L1范数最小化问题,实现对目标的跟踪。

MIL

MIL采用了多示例学习的方法而不是传统的监督学习(即由原本单独标记的示例变成一组示例,当且仅当所有的示例都判定为负才认为是负,只要有一个示例判定为正则整组都判定为正),对于不精准的tracker和错误标注的训练样本有鲁棒性的提升。

2012

CSK

CSK也称为核相关滤波算法,作者针对MOSSE做出了一些改进,作者认为循环移位能模拟当前正样本的所有的转换版本(除边界以外),因此采用循环移位进行密集采样,并通过核函数将低维线性空间映射到高维空间,提高了相关滤波器的鲁棒性。这里循环移位后的样本匹配可以理解为如果某个候选区域与某一个移位样本的相关操作响应较高,那么就可以理解为物体的移动和该样本移位的方式一致,从而对下一帧目标位置进行定位。
随后的工作主要从特征选择、尺度估计、正则化等方面对该算法进行改进和提高。关于循环移位和线性、非线性的核函数计算,我在之前的文章中做了一些分析,感兴趣的话可以看看。

DF

DF发现之前在图像中寻找目标的梯度下降方法首先会模糊图像来平滑目标方程,这就会严重损害目标的位置信息。因此作者提出了对每一个像素点设置多个通道,在每个通道进行卷积,这种方法同样也能平滑目标方程但不会严重损害目标的位置信息。其实这就是之后的CNN能做到的,在当时应该也算是一种创新。

CT

CT(Compressive Tracking)是一种基于压缩感知的高效跟踪算法。和一般的判别式模型架构一样,CT首先利用符合压缩感知RIP条件的随机感知矩阵对图像特征进行降维,使得到的低维信号可以完全保持高维信号的特性并可以完全重建,然后在降维后的特征上,在感知空间下采用朴素贝叶斯分类器进行分类。另外,CT在每一帧通过在线学习更新分类器,在线学习的样本来自通过相同的稀疏感知矩阵提取的前景目标和背景的特征。

ORIA

ORIA假设前一帧是完美的,于是把跟踪问题视作将下一帧图像与上一帧进行对齐,也就是一串连续的凸优化问题。

2013

下面是VOT2013的排名结果,其中Experiment 1是在所有序列上使用ground truth初始化的实验结果,Experiment 2使用含噪声(10%的尺寸扰动)的ground truth,Experiment 3使用灰度图像。

PLT

PLT通过一个固定大小的、基于二值特征向量的线性分类器对每一张图像做分类,得分最高即为目标。作者利用一个稀疏的在线结构化SVM来选出一个小的判别特征集合。在训练SVM时,考虑到在bounding box内的像素不一定都属于物体,作者使用了一种基于概率的掩模来分配权重,然后计算初始的结构化SVM,去除分值最小的特征。由于特征向量的二值性,该线性分类器可以作为查找表用于快速检测。

EDF

EDF是DF的加强版,在DF的基础上探索了每一个通道之间的联系。

LGT

LGT针对模型何时更新与更新哪些部分的问题,考虑到目标模型的整体更新会损失部分有用信息和固定的分块不利于应对目标的变化,借鉴了之前将目标有结构地分块且动态删减的思想,提出了一种由patch组成的集合构成的、用于精确定位的local layer和颜色、移位、形状三个特性组成的、用于指导增加patch的global layer。

对于新输入的一帧图像,LGT的处理流程如下:

  1. 首先使用卡尔曼滤波器结合近似匀速模型来确定目标的位置。接下来几步是对位置进行微调。
  2. 对于每一个patch,作者用一个统一的仿射变换和一个独立的微小扰动在5维空间(位置2维+尺度2维+旋转1维)定义其对于初始patch的变换。这里的$\widehat{x}$指的是初始patch。作者把$A_{t}^{G}$和$\Delta _{t}$中的参数看作正态分布的。先使用交叉熵方法反复迭代寻找最优解,当协方差矩阵的行列式小于0.1(各分量相关性很强)时停止迭代,随后把仿射变换矩阵的参数(也就是学到的正态分布的均值和方差)固定并用于所有的patch。接下来对每一个patch,用同样的交叉熵方法迭代,得到每一个patch的微小扰动的均值和方差。
  3. 结合visual consistency和drift from majority两种估计,更新每一个patch在当前帧的权重,并与前一帧的权重加权求和来确定最终的权重。这里的权重决定了每一个patch在最终所有patch混合决策时的重要性。
  4. 利用上面的patch重新计算之前卡尔曼滤波器的结果,确定目标的位置。
  5. 接着进行local layer中patch的删减与增补。对于权重小于阈值的patch作删去处理;对距离很近的两个patch进行合并,合并后产生的patch的所有参数设为合并前两个patch参数的平均。使用剩下的patch更新global layer,然后用更新后的global layer决定是否以及如何增加新的patch。为了防止突然过度增加patch,作者对增加的样本数施加上限限制,并利用加权的方式平滑调整样本容量。
  6. 进入下一帧。

LGT++

LGT++是LGT的改进版。在LGT的基础上,LGT++增加了memory、failure detection和用粒子滤波代替卡尔曼滤波的recovery机制。此外对尺度变化和背景干扰也做出了改进。

DLT

DLT是最早的基于深度学习的算法(当时AlexNet刚刚被提出),它采用了堆叠去噪自编码器网络,把跟踪视为一个分类问题,直接利用80 Million Tiny Images数据集上的预训练模型提取深度特征,这种强行task转换的训练方法存在缺陷,但在当时是个进步。

STC

STC(Spatio-Temporal Context)通过贝叶斯框架目标时间、空间的上下文信息来建模,利用得到的关系结合生物视觉系统中的注意力特性来生成confidence map来预测目标位置。由于上下文信息建模和之后的预测都采用了快速傅里叶变换,因此算法的速度很快。

文章主要举了两个例子来说明空间信息的重要性。当目标物体被部分或者完全遮挡时,周围的信息能帮助定位被遮挡的目标(假设摄像头不移动),也就是说可以利用空间的距离信息;此外,如果目标内部的两个部分比较相似(比如人的一对眼睛),就比较容易发生偏移,而如果这时恰好这两个部分的距离信息相似(距离目标中心长度相同),那就需要引入相对位置也就是方向信息来判断。
此外,考虑到生物视觉系统中的注意力特性,作者增加了一项权重函数来构成先验,该函数根据距离目标位置的远近来定义。
作者还对confidence map的参数进行了讨论,认为置信度在空间上的分布不能太平滑(增加位置模糊不确定性),也不能太尖锐(导致过拟合)。
作者认为目标的形态与近几帧有较强的关联,由此设计了时域信息模型。文中提到的时域滤波器可被证明是低通的,也就是可以滤去一定的噪声。此外,STC还设计了尺度更新方法,最终下一帧的尺度是前n帧估计尺度的均值。

LT-FLO

LT-FLO主要针对的是缺少纹理特征的目标,使用边界点来代替在目标上采集点来做跟踪。此外作者还提出了一种基于边界梯度稳定性的failure检测机制。

GSDT

GSDT提出了一种采集正负样本进行图嵌入的判别式模型。作者使用了基于图结构的分类器而不是生成一个子空间。此外GSDT还设计了一种新的图结构来区分类内的不同和样本的内在结构。

SCTT

SCTT使用了treelets降维方法,由于仅选取较高置信度的样本,相对于PCA只需要更少的样本且对噪声有更好的鲁棒性。

CCMS

CCMS(Color Correspondences Mean-Shift)用之前提到的Mean Shift方法对目标候选与目标模型、目标候选与背景模型在每种颜色(也就是直方图中每个bin)中计算相似性,反复迭代,直到收敛或者达到最大迭代次数为止,如此来进行运动估计。

Matrioska

Matrioska基于特征点提取的方法(ORB、FREAK、BRISK、SURF等),考虑到目标物体的外观变化和有利于增强模型表现力的负样本提取,使用了增枝和剪枝的方式来对目标模型进行更新。

AIF

AIF(adaptive integrated feature)提出了一种评估特征稳定性的方法,并根据不同特征的稳定性动态地分配权重。

HT

HT借鉴霍夫森林,也就是霍夫变换结合随机森林,相比一般的随机森林增加了位移信息。HT将目标分割成多个图像块,这些图像块含有它们各自偏离目标中心的向量$d$,对每个图像块提取特征描述子,这样就构成了正样本,即$y=1$;在图像的其他区域也提取同样尺寸的图像块,也提取特征描述子构成负样本,即$y=0$,注意负样本的偏移向量$d=0$。由此训练生成树,再由树构成森林。
在跟踪时,将每个图像块输入训练好的森林里,最终会落到森林的每棵树的一个叶节点上,这就得到了该图像块相对目标中心的偏移向量$d$以及概率$p$,随后将每棵树上的结果加权平均,得到了该图像块的结果。最后将所有的目标分割出的图像块的结果组合起来,得到目标预测结果。
HT的一个好处就是可以调整bounding box的长宽比,此外作者认为这对非刚性或者铰接的目标也有很大好处。

STMT

STMT把目标跟踪分成镜头运动估计和目标运动估计两个阶段,先估计摄像头的运动并进行对齐,然后再定位下一帧的目标位置。

ASAM

ASAM(Adaptive Sparse Appearance Model)用一个样本集来表示目标的各种变化,并且基于判别式和生成式的稀疏表示,使用了第一帧、后续帧两阶段的在线跟踪算法。

2014

下面是VOT2014的排名结果,这里的A表示accuracy,R表示robustness。

DSST

DSST主要考虑了尺度缩放的问题。它将目标跟踪看成位置变化和尺度变化两个独立问题,提出了一个高、宽、尺度数三维的滤波器,使用先计算平移位置再聚集尺度的“两步”法,即训练了两个滤波器,首先训练位置平移相关滤波器以检测目标中心平移,然后训练尺度相关滤波器来检测目标的尺度变化。

CN

CN(Color Naming)考虑到在遇到光照变化、形变、部分遮挡、背景干扰等问题时,颜色特征相比灰度特征能提供更丰富的信息以取得更好的效果,引入了颜色特征来扩展CSK,它将目标RGB(红绿蓝)三维空间的颜色特征映射为黑、蓝、棕、灰、绿、橙、粉、紫、红、白和黄11维空间的颜色特征的多通道颜色特征,后又降维至2维以保证实时性。Color Naming较RGB三原色特征更符合人类的感觉,对目标的表征能力更强,而且具有一定的光学不变性。

SAMF

SAMF也考虑了尺度问题,思路比较简单,采用k个尺度去采样,由于核相关操作的点乘需要固定尺度的输入,因此对采集到的样本作双线性插值成为固定尺度,然后再做相关操作。在特征方面,SAMF发现HOG和Color Naming有互补作用,考虑到和相关操作仅包含点乘和向量范数的计算使得多通道很容易被引入,因此使用了HOG和Color Naming多通道特征。

KCF

KCF跟CSK是同一个团队提出的,它跟CSK的区别是就是作者对循环性质进行了完整的理论推导,引入HOG特征并提供了一种把多通道特征融合进相关滤波框架的方法,对CSK作了进一步的完善,是一个具有里程碑意义的工作。算法的详解和一些数学理论可以看看我之前的文章。

DCF

DCF与KCF出自同一篇paper,不同的是KCF使用的是高斯核,DCF使用的是线性核。

注意:这里的DCF(Dual Correlation Filter)和之后一些文章中提到的DCF(Discriminative Correlation Filter)是两个不同的概念,请注意,别搞错了。

FCT

FCT(Fast Compressive Tracking)和CT一样,也是使用了压缩感知,主打速度。相比之前,FCT提出了一种由粗到精的搜索策略,而不是穷尽搜索。首先在一个较大的搜索半径内选择一个较大的搜索步长,得到一个粗糙的位置,然后以该位置为中心,在一个较小的搜索范围内,以一个较小的搜索步长进行搜索,最后得到跟踪目标的位置,这样就能在不降低最终精度的前提下加速寻找过程。由于可证明CT特征具有尺度不变特性,FCT在采集候选区域时增加了尺度因子,即在同一位置采集三个尺度的候选区域,从而得到当前帧的尺度。此外,作者采用了每5帧更新一次尺度的策略。

CMT

CMT用成对的特征点之间角度的变化来判断目标的旋转情况,此外还用特征点投票的方式来确定目标的位置。为了避免尺度变化引起的投票不准,也就是从特征点出发指向目标位置的投票向量越过了目标中心或者没达到目标中心,作者用欧氏空间和原图像空间之间各对特征点之间距离的比值来进行修正。基于投票出的目标位置的聚类,作者还给出了一种一致性的判别方法,将聚集最多数特征点的投票位置视为一致性聚类,并将投票至其他位置的特征点视为错误从而移除。

2015

下面是VOT2015的排名结果。

SO-DLT

SO-DLT针对DLT的缺陷进行改进,使得CNN更加适用于目标跟踪。由于目标跟踪的目的是将物体从背景中分离出来而不是全图识别,DLT的训练方法和标签化输出就不是很合适了。
但是,由于当时跟踪方向标注数据的匮乏,作者还是不得不使用ImageNet图像检测数据集来进行预训练,事实证明这是有效的,因为目标检测和目标跟踪两个不同的task中存在一样的共性信息。不同于标签化的单个数值输出,SO-DLT输出的是一个50x50的像素级的概率图。
由于上述训练方法训练的是CNN从非物体中提取出物体的能力,因此在实际跟踪接收到第一帧时,还需根据目标对网络进行微调,否则会跟踪出视频或者图像序列中所有的无论是目标与否的物体。
类似DSST,SO-DLT采用的是先预测目标中心位置,然后再从小到大确定尺度的策略,如若扩展到预设的最大尺度来检测的概率图依旧达不到要求,则认为已经跟丢目标。
此外,为了提升鲁棒性,作者采用了两个CNN网络共同决策而以不同方式更新的策略。两个网络分别针对的是short-term和long-term。针对short-term的网络在负样本的概率图响应和超过一定阈值时进行更新,为的是防止负样本与目标响应近似而导致漂移;针对long-term的网络在当前帧预测结果的置信度达到一定水平以上时才进行更新,因为此时可认为框出的目标较为可信。
每次更新时需要采集正负样本,SO-DLT对正负样本的提取方法比较简单,在目标位置及周围形成一个类似九宫格的区域,在中间格用四种尺度提取正样本,对周围的8格采集负样本。

MDNet

MDNet设计了一个轻量级的小型网络学习卷积特征表示目标。作者提出了一个多域的网络框架,将一个视频序列视为一个域,其中共享的部分用来学习目标的特征表达,独立的全连接层则用于学习针对特定视频序列的softmax分类。
在离线训练时,针对每个视频序列构建一个新的检测分支进行训练,而特征提取网络是共享的。这样特征提取网络可以学习到通用性更强的与域无关的特征。
在跟踪时,保留并固定特征提取网络,针对跟踪序列构建一个新的分支检测部分,用第1帧样本在线训练检测部分之后再利用跟踪结果生成正负样本来微调检测分支。
此外,MDNet在训练时还采用了难例挖掘技术,随着训练的进行增大样本的分类难度。

SRDCF

SRDCF主要考虑到若仅使用单纯的相关滤波,可能会存在边界效应,也就是相关滤波采用循环移位采样导致当目标移位到边缘时会被分割开,此时得到的样本中就没有完整的目标图像从而失去效果。
于是,作者采用了大的采样区域,用两倍区域进行循环移位,这就保证了无论如何移位,获得的样本中都能有一个完整的目标存在。
然而,由于上述移位方式会导致背景信息被夹在横向以及纵向的两个样本之间而出现在图片的中间区域,这会导致模型判别力的退化。因此作者在移位之前先在滤波器系数上加入权重约束(类似于惩罚项):越靠近边缘权重越大,越靠近中心权重越小。这就使得滤波器系数主要集中在中心区域,从而让背景信息的影响没有那么明显。尽管作者采用了埃尔米特矩阵的共轭对称性来提升计算量,但是由于SRDCF破坏了原本闭式解的结构使得优化问题只能通过迭代求解,因此速度比较缓慢。

DeepSRDCF

DeepSRDCF在SRDCF的基础上,将handcrafted的特征换为CNN的特征,关注点也在解决边界效应。作者使用DCF作为网络的最后一层,也就是对之前卷积网络输出的每一个通道的CNN特征都训练一个滤波器用于分类。作者还对不同的特征进行了实验,说明了CNN特征在解决跟踪的问题采取底层的特征效果会比较好(DeepSRDCF仅用了PCA降维处理的第一层),说明了跟踪问题并不需要太高的语义信息。

HCF

HCF的主要贡献是把相关滤波中的HOG特征换成了深度特征,它使用的是VGG的3、4、5三个层来提取特征,针对每层CNN训练一个过滤器,并且按照从深到浅的顺序使用相关滤波,然后利用深层得到的结果来引导浅层从而减少搜索空间。

FCNT

FCNT较早地利用CNN网络底层和顶层不同的表达效果来做跟踪。不同于以往的工作把CNN看成一个黑盒而不关注不同层的表现,FCNT关注了不同层的功能,即发现:顶层的CNN layer编码了更多的关于语义特征的信息并且可以作为类别检测器;而底层的CNN layer关注了更多局部特征,这有助于将目标从目标中分离出来。这个发现在之后的许多工作中也得到了应用和体现。如下图所示,这里的a图表示的是ground truth,b图表示的是使用VGG的conv4-3,也就是第10层产生的热力图,c图是通过conv5-3也就是第13层产生的热力图。

可以看到,较低维的CNN layer(conv4-3)能够更精准地表示目标的细粒度信息,而较高维的CNN layer(conv5-3)热力图显示较模糊,但对同类别的人也做出了响应。这就是说,顶层缺少类内特征区分,对类间识别比较好,更适合作语义分割;底层则反之,能够更好地表达目标的类内特征和位置信息。
基于不同层(顶层和底层)之间提取特征的不同,作者提出了一种新的tracking方法,利用两种特征相互补充辅助,来处理剧烈的外观变化(顶层特征发挥的作用)和区分目标本身(底层特征发挥的作用)。由于feature map本身是有内在结构的,有很多的feature map对目标的表达其实并没有起到作用,因此作者设计了一种方法来自动选择高维CNN(GNet)或者低维CNN(SNet)上的feature map,同时忽略另一个feature map和噪声。在线跟踪时,两个网络一起跟踪,采用不同的更新策略,并在不同的情况下选择不同的网络输出来进行预测。
顺便提一下,为了简化学习任务,降低模型复杂度,作者采用了稀疏表示的方法。
关于FCNT的一些相关概念和具体按步骤的细节实现,可以参考一下我之前写的文章。

LCT

LCT主要针对的是long-term tracking的问题。作者配置了一个detector,用于跟丢之后快速重检测。LCT用了两个滤波器,一个是用于平移估算的$R_{c}$,使用padding并施加汉宁窗(一种余弦窗),结合了FHOG和一些其他的特征;另一个是用于尺度估计的$R_{t}$,不使用padding和汉宁窗,使用HOG特征,此外$R_{t}$还用于检测置信度,用来决定是否更新模型和是否重检测。

CCT

CCT借鉴KCF中kernel trick的特性和DSST中将定位和尺度估计两步分离的思想,对DSST只利用本来的特征空间表征目标的不足进行改进,在核特征空间对目标进行表征并用尺度因子扩展KCF的核相关滤波器,也就是为了保证计算效率和连贯性,利用尺度因子将每一帧目标尺度都统一为初始帧的目标尺度,然后使用核相关滤波器进行位置估计。然后,通过和DSST一样的方式再进行尺度估计。
为了应对漂移的问题,CCT使用了一个在线CUR滤波器。CUR矩阵分解可以近似的表示原矩阵A,其中C是A的列而R是A的行,两者通过一种固定的方式从A中随机采样形成,这既保证了A的内在结构,可以反映A的低秩属性,也可以看作一种映射,即将过去的目标表征矩阵投影到一个可被证明具有误差上界的子空间。此外,作者还引入了一个基于失败检测的自适应学习率调整方法。

CFLB

CFLB讨论了循环移位带来的边界效应的问题,提出了在目标外围扩大尺寸来进行循环移位的方法,使得有效样本的比例大大提高。具体来说,就是给原来的循环移位样本左乘一个列数远大于行数的掩模矩阵。此外,由于扩大尺寸后部分参数要在空间域而不是频域计算会导致效率降低,作者利用了增广拉格朗日方法(即在拉格朗日方法的基础上添加了二次惩罚项,从而使得转换后的问题能够更容易求解,不至于因为条件数变大不好求)来解决这个问题。

KCFDP

KCFDP借鉴了目标检测中detection proposal的思想(主要用于减少计算和提高质量),来解决之前DCF系列算法中的尺度和长宽比变化的问题。对每一帧,KCFDP首先用KCF对上一帧输入(准确的说是之前每一帧的加权累积)作操作,得到当前帧的位置和响应分数$v$;随后利用EdgeBoxes(一种detection proposal方法)在KCF预测的位置周围搜寻proposal,选取其中的前200个,并排除其中与之前KCF得出的预测目标IoU大于0.9(认为结果一样,无需考虑)或者小于0.6(认为误判,不是目标);最后,在剩余的proposal中,选择得分最高的proposal,与之前的响应分数$v$作比较:若小于$v$,则把KCF的结果作为预测结果且不更新尺度和长宽比(KCF算法本身具有该功能);若大于$v$,则把该proposal作为预测结果,并利用该proposal的尺度和长宽比来更新目标的参数。

HCFT

HCFT构造了一种阶梯式的深层至浅层由粗到细的定位方法,结合深层网络的语义信息和浅层网络的高分辨率位置信息,其网络结构如下(和FPN很像)。

为了保持分辨率相同,作者对池化后的深层输出再进行双线性插值以复原原来的分辨率。假设$l$层最大响应处的坐标为$(\widehat{m},\widehat{n})$,HCFT通过以下式子来确定$l-1$层目标的位置。

第二行的约束是为了浅层细粒度的位置需保持在深层粗粒度的位置附近,这样便完成了由粗到细的定位方法。
此外,在训练过程中需要采集正负样本,由于正负样本边界难以区分的模糊性和二值(也就是0,1标注)的正负样本的绝对性导致一点微小的正负样本区别就会导致drift。为此,作者将训练样本的标注回归到高斯方程的平滑标签。

MUSTer

MUSTer模拟了人脑的记忆过程,类似于LSTM那样分成short-term和long-term两种memory,使用了相关滤波(short-term)和特征点检测(short-term+long-term),最后根据两种记忆形式的提供的输出来决策和进行滤波器的更新。
人脑的记忆分为感官、短时记忆、长时记忆三个阶段,MUSTer的设计基本采用了这样的三个step,如下图所示。

MUSTer的结构比较“纵横交错”,下面我对一些比较重要的部分做一个概述。
短时记忆和长时记忆都由特征点的集合构成。特征点数据集包括目标和背景两种样本,其中背景主要是用于遮挡的判断,即当位于bounding box中的背景特征点与目标特征点的比例超过一定值时,认为此时发生了遮挡。
对于特征点的匹配,作者采用了最近邻方法,在欧几里得空间根据余弦相似度(也就是两个向量余弦夹角的大小,角度越小,余弦值越大,相似度越高)来计算匹配置信度。为了判别离群值(outlier),还需计算第二近邻的相似度,如果第一近邻的相似度比上第二近邻的比值小于某个阈值,就说明该处特征点比较集中应该不是离群值。
长时记忆模块通过RANSAC估计的一个版本——MLESAC(引入似然度)来决定目标的状态,从而与相关滤波的输出结合。
短时记忆在每一帧都进行更新,若根据前面所说的方法判定为遮挡,则清空短时记忆;若此时并没有判定为遮挡,则用RANSAC估计输出的内围值(inlier)来替换之前的短时记忆。此外,为了避免多余的特征点出现,作者用网格划分目标template并根据相对位置分配ID,若出现重复的ID,则认为两者中之前的特征点是多余的。
长时记忆只在判断跟踪成功和无遮挡时进行更新。作者认为匹配失败的特征点能够表示目标发生变化的重要信息,因此长时记忆更新是针对匹配失败的点进行的,将匹配失败且位于bounding box外面的点移入背景数据集,而将匹配失败且位于bounding box内部的点移入目标数据集。模拟人脑,长时记忆采用的是一种对数形式下降的遗忘曲线。

RPAC

RPAC将相关滤波器应用于分块跟踪,并且借鉴了粒子滤波中的贝叶斯估计的思想,在提升鲁棒性的同时保证了速度。
作者对每一区块(part)都使用一个独立的相关滤波器,使用了PSR(体现置信度)和时域顺滑程度(用于判断遮挡等情况)两者结合来分配每一区块的权重,同时仅当这个权重大于阈值时才更新对应的滤波器以达到自适应更新的效果。这里每一个区块的输出都是一个confidence map,最后需要根据权重和相对位置组成一个大的confidence map用于接下来的预测。
为了防止部分区块漂移的问题出现,作者采用了贝叶斯估计框架,即选择使得状态(一组仿射运动参数)先验值最大的候选区域作为结果,考虑到多个confidence map之间重叠的部分直接求和会出现叠加的较大值而影响概率估计,作者根据每一个区块的最大响应值和尺寸来施加余弦窗,从而抑制多张图中较小值的叠加改变某些区块位置上的响应分布。
此外,对于偏离较远的区块,RPAC采用自动丢弃并利用其他区块重新生成的方法。得益于分块的方式,使得tracker对尺度变化也有适应性。

RPT

RPT把目标物体看作一系列具有相似运动轨迹的patch的集合。作者基于粒子滤波的框架,用patch的两个属性来定义每个patch的可靠性:可追踪性和目标附着性。可追踪性直接用KCF输出的响应图取PSR来定义;目标附着性根据每个patch在近k帧的运动轨迹来定义,具体来说,就是认为正样本与其他正样本的运动轨迹是一致的且远离负样本,同时负样本与正样本的运动轨迹有很大的差别。其公式定义如下,其中$y_{t}$是正样本或者负样本的$\pm 1$标签(正负样本用bounding box来分割)。

由于粒子滤波是用函数(这里是可靠性函数)对后验概率分布做近似,由于无法达到理想状态(即完全一致),因此为了避免噪声的累积,粒子滤波类算法需要重采样来不断补入正确的信息。RPT采用的是不断补入新样本的方法而不是全部重采样替换的方法,作者在两种情形下采样新的样本:当正样本或者负样本其中一者的比例过高时,采集新样本来平衡;当跟踪置信度(这里定义为PSR)较低时,采集新样本,这种情况往往出现在遇到缺乏纹理的目标物体时。此外,作者对离目标过远的patch进行舍弃,具体就是划定一个比bounding box更大的矩形框,对超出这个矩形框的patch做舍弃。
可以说,作者把patch分成三类。第一类是positive patch,也就是在目标上的;第二类是贴着目标周围一圈的negative patch,这对下一帧区分物体和背景很有帮助;第三类就是离目标很远的negative patch,这些patch的作用就比较弱,因此舍弃。

2016

下面是VOT2016的排名结果。

DLSSVM

DLSSVM延续之前的Struck,利用结构化SVM,在优化的阶段做了一些改进进行提速。其实结构化SVM分类器非常强大,但是因为它求解优化的过程比较复杂以及使用稠密采样(粒子滤波或者滑窗采样)比较耗时,使得结构化SVM的速度成为一个瓶颈,因此不如一些使用相关滤波的SOTA的算法。

C-COT

C-COT(连续空间域卷积操作)发现单一分辨率的输出结果存在扰动,因此作者想到利用CNN中的浅层表观信息和深层语义信息相结合。然而之前的DCF系列算法仅能使用单一分辨率的特征图,这就无法使用预训练CNN中不同分辨率的不同层,这是限制其效果的重要因素。因此,作者提出一种连续周期的插值运算符以利用不同空间分辨率的响应,在频域进行插值得到连续空间分辨率的响应图,最后通过迭代求解最佳位置和尺度(用0.96,0.98,1.00,1.02,1.04五种缩放倍率去搜索)。
和名称一样,C-COT最重要的贡献是它把图像$N_{d}$个像素点的离散分布变成了周期为$T$的连续空间响应图。由于本文的理论功底很深,我之前并没有看懂,后来经学长的指点才有所感悟。事实上,这里的插值运算符做的并不是插值的事,而是用一种近似的方式将离散信号重构为连续信号。虽然会导致计算量剧增,但这是一个很重大的突破。
文中还提出了一种使得各个分辨率通道的特征自然融合至相同分辨率的方法,这里相同分辨率可理解为最后的各个响应图在空间上拥有相同的样本点数。作者首先对各个不同分辨率的通道进行插值,然后使用对应的滤波器在连续的空间域内卷积,最后将响应求和得到最终的置信度方程。
C-COT使用的是类似SRDCF的框架,也引入了空间正则项,当远离目标中心是施加较大的惩罚,这使得能够通过控制滤波器的大小来学习任意大小的图像区域。C-COT在每一帧也会采集一个训练样本,根据过去帧数的远近来设置每个采集样本的重要性权重(每次都做归一化),并且设置了最大的样本容量,当超出容量时删去重要性权值最小的样本。不同于SRDCF使用Gauss-Seidel迭代法,C-COT使用Conjugate Gradient方法来提高效率。
得益于浅层特征的高分辨率,C-COT能够达到sub-pixel的精度,也就是仅次于像素级别的精确度。位置细化的过程就是上面所说的用共轭梯度法迭代的过程,在C-COT的代码中有一个迭代次数设置,被设置为1,即就使用一步迭代优化后的位置。换句话说,在当前长时跟踪算法本身误差之下,更精细的位置意义不大。

SRDCFdecon

SRDCFdecon针对在线跟踪时采集的样本中有一部分质量不佳的问题,不同于之前把采样和样本的选择作为一个独立的模块,作者提出了一种将样本权重统一到模型参数中的损失函数。
不同于之前“加入训练集or舍弃”这样二选一的样本选取方式,SRDCFdecon使得样本的重要性权重连续,同时在跟踪的过程中能够完成权重的重新分配和先验的动态变化。这里的先验其实可以看作对权重的一种约束,使权重在近几帧逐渐增大。作者用一种比较巧妙的方式来控制先验的影响力,具体来说,对权重的正则项为$\frac{1}{\mu }\sum_{k=1}^{t}\frac{\alpha _{k}^{2}}{\rho _{k}}$,当$\mu$趋向于无穷时,损失函数求解得到仅有当前帧的权重$\alpha$趋向于1,相当于丢弃了之前的样本,仅接收并保存当前帧的新样本;当$\mu$趋向于零时,$\alpha$将趋向于先验$\rho$。

Staple

Staple提出了一种互补的方式。考虑到HOG特征对形变和运动模糊比较敏感,但是对颜色变化能够达到很好的跟踪效果,color特征对颜色比较敏感,但是对形变和运动模糊能够有很好的跟踪效果,因此作者认为若能将两者互补就能够解决跟踪过程当中遇到的一些主要问题。于是,Staple使用HOG-KCF与color-KCF结合算法对目标进行跟踪,速度很快,效果也很好。

SINT

SINT运用匹配学习的思想,最早地把孪生网络(Siamese Network)应用于目标跟踪。它通过孪生网络直接学习目标模板和候选目标的匹配函数,并且在online tracking的过程中只用初始帧的目标作为模板来实现跟踪。

TCNN

TCNN使用一个树形的结构来处理CNN特征。作者利用可靠性来分配预测目标的权重,采用的更新策略是每10帧删除最前的节点,同时创建一个新的CNN节点,选择能够使新节点的可靠性最高的节点作为其父节点。这样一直保持一个active set,里面是10个最新更新的CNN模型,用这个active set来做跟踪。TCNN效果较之前有一定提升,但是速度比较慢,而且比较消耗存储空间。

SKCF

SKCF把之前KCF中用于确定搜索区域的余弦窗换成了高斯窗,这么做有两点主要的好处。
首先,当搜索区域固定时,余弦窗的带宽就是固定的了,而高斯窗则可以通过调整方差来改变中间响应比较高的区域的宽度。可以这么理解,我们从二维的余弦函数和高斯函数来看,假设搜索区域的宽度是$\pi $,那么距离边缘$\frac{\pi }{6}$的位置一定是中间最大值下降一半的位置,而高斯函数的形状则还受方差控制。这一特性使得高斯窗在搜索区域确定时能够更好地适应目标尺寸,此外论文中还提到了这种方法能方便减轻计算量。
此外,高斯分布的傅里叶变换依旧是高斯分布。这种特性可以抑制频谱泄露的问题。简单来说,频谱泄露就是信号从时域转化到频域后,除了原本应该出现的谱线,其旁边还会漏出一些小的频谱,从而造成干扰。因此,抑制频谱泄露能够保持前景与背景在频域内的区分度。
SKCF还改进了之前一些基于特征点的算法的不足。由于之前的一些算法在最终决策时用矩形框来划定有效的特征点,并分配相同的权重做出决策。作者认为这样相当于间接的认为目标是矩形的,而大多数目标的几何结构往往不是矩形。于是SKCF对各个特征点采用从中心到周围逐渐减少权重分配的方式,能更好地适应目标的几何结构。
SKCF还是用了英特尔CCS(复数共轭对称)文件格式,无论是用在SKCF还是KCF上,计算速度相比原来的KCF都提升了将近一倍,或许是共轭对称减少了一半计算量。

MRF

MRF提出了一种基于马尔科夫随机场的模型,挖掘各个patch和目标之间的联系(弹性能量)并判断每个patch的遮挡情况。作者发现当响应值较低时并不能认为该patch被遮挡了,也有可能是目标外观变化等情况。此外作者还发现当发生遮挡时,会出现较大的响应值分散分布的现象。于是当patch中响应值高于$\eta s_{max}^{k}$的像素个数占比高于阈值时(这里的$\eta$是一个小于1的因子),就认为该patch被遮挡。
对于尺度变化,作者发现当尺度变小时patch之间的重叠会变多,因此通过计算初始帧的patch距离和当前帧的patch距离之比来确定尺度缩放的比例。
不同于之前的马尔科夫随机场模型,作者在文中还给出了一种高效的置信度传播方法。

GOTURN

GOTURN提出了一种类似孪生网络的框架,将裁剪过的前一帧和当前帧分别通过在ImageNet上预训练的backbone提取特征,然后用三层全连接层进行特征比较并进行回归。其中前一帧的图像在裁剪时将其置于裁剪后图像的中心,并在周围做一定的扩充以吸收更多上下文信息。当前帧裁剪区域也就是搜索区域由上一帧的位置来确定。
GOTURN最突出的贡献就是把基于深度神经网络的速度第一次达到了100FPS。作者通过在视频和静态图像上进行离线训练,在跟踪时不更新来达到这种效果。由于训练集的缺乏,作者冻结之前在ImageNet上预训练的CNN权重,在离线训练时仅更新FC层。考虑到实际跟踪时的特点,作者还设计一种平滑的运动模型。作者发现目标相邻两帧中心点的坐标关于尺度的增量呈均值为0的拉普拉斯分布(类似高斯分布,只不过中间是尖的),也就是说一般物体逐帧的运动是较小的。因此,作者对训练数据做增广处理,使得随机裁剪得到的样本能服从拉普拉斯分布。由于训练方式和网络设计,使得GOTURN仅对目标敏感而不是对类敏感(比如行人检测能检测各种行人而不能检测车)。

SiamFC

SiamFC使用孪生网络来解决数据稀少和实时性要求对深度学习在目标跟踪中的限制。作者以第一帧的BBox的中心为中心裁剪出一块图像,并将其缩放至127x127作为template,并保持不变。在后续帧中,search image也用类似的方法得到。分别将template和search image通过5层不带padding且不在线更新的AlexNet,然后用互相关层做相关操作得到输出的score map。响应最大值和中心的偏差表示位移。此外作者用一个小批量的不同尺度的图像去前向传播来实现多尺度判断。
由于对于search image来说,表示通过CNN的卷积方程是全卷积的,因此可以使用比template大的search image,也就是可以在它上面的各个子窗口进行计算。
基于卷积的平移等变性,我们可以通过score map得到目标的位置,即当目标平移了$n$时,相应的就会在score map上平移$\frac{n}{stride}$。之所以仅使用5层而不是更深的网络,是因为SiamFC没有使用padding来使得网络能够更深。之所以不使用padding,是因为一旦加入了padding,会使得图像边缘像素的响应值在平移的同时会发生改变,这就不利于最后的定位了。具体来讲,就是当目标处于画面中央时,padding进来的是context;而当目标处于画面边缘时,padding进来的就是0了,这种信息的不同会影响定位和目标的判断。

2017

下面是VOT2017在隐藏数据集上的排名结果。

ECO

ECO(高效卷积算子)主要是为了解决C-COT速度慢的问题。从参数降维、样本分组和更新策略三个角度对其改进,在不影响算法精确度的同时,将算法速度提高了一个数量级。
为了减少模型参数,考虑到在C-COT中许多滤波器的能量(可理解为贡献)小得几乎可以忽略,因此ECO使用了一个较小的滤波器子集,且原来的滤波器都可以用这个子集中的滤波器线性组合表示(即乘上一个行数为高维数、列数为低维数的矩阵)。子集的选取方法就是简单的选取能量高于某一阈值的滤波器,其效果类似于PCA。作者提出了一个因子化的卷积算子来学习这个子集,用PCA初始化,然后仅在第一帧有监督地优化这个降维矩阵,在之后的帧中直接使用,相比C-COT模型参数量大大降低,同时也减轻了计算和存储的负担。

为了减少样本数量,作者提出了一个紧凑的样本空间生成模型,采用高斯混合模型(GMM,可理解为当有多个聚类时用多个不同的高斯模型来表示更好)来合并相似样本。当GMM的聚类(component)数量超过阈值时,如果有权重低于阈值的component,则丢弃之;否则,就合并最近的两个component。如此就可以建立更具代表性和多样性的样本集,既保持样本之间的差异性,也减少了存储的样本数量。

此外,作者还提出了一种稀疏的更新策略,即每隔N帧(实验发现5帧左右最好)才更新一次参数。由于样本集是每帧更新的,这种稀疏更新策略并不会错过间隔期的样本变化信息。此外,这种方法的另一个好处就是把原本的用逐帧单独样本进行更新,变成了用连续几帧所采集的样本所构成的batch来进行批处理的更新,这样就减小了在某帧遮挡、突变时过拟合的可能性。由此,稀疏更新策略不但提高了算法速度,而且提高了算法的稳定性。

CREST

CREST提出了将DCF构建成一层卷积神经网络,并且引入了残差学习来应对目标外观变化带来的模型退化。
考虑到之前的DCF系列没有发挥端到端训练的优势和空间卷积与相关滤波中循环输入点乘的相似性,作者用一层卷积神经网络来代替DCF的作用,这不仅使得模型能够通过反向传播训练,同时因为没有使用循环移位,避免了边界效应。
由于上述一层网络难以达成在多种情况下网络输出和ground truth的一致(模型复杂度较低易受干扰),而若使用多层网络很可能会导致模型退化(我理解为过拟合导致的),作者引入空间残差和时间残差。设我们希望最佳的输出为$H(x)$,而上述单层网络的输出是$F_{B}(x)$,为了补足某些时候(尤其是复杂情况下)单层网络的输出与希望最佳的输出之间的差距,引入残差项$F_{R}(x)=H(x)-F_{B}(x)$。在训练时,$F_{B}(x)$和$F_{R}(x)$中的参数一起训练,使得遇到特殊情况(遮挡、运动模糊等)时,$F_{R}(x)$能够补足纠正$F_{B}(x)$不稳定的响应结果。
空间残差和单层网络都是利用当前帧作为输入,考虑到空间残差有时候也会失效,作者又引入了把初始帧作为输入的时间残差,最终表达式如下:

注意:论文中第一项为$F_{R}(X_{t})$,可能有误,为此我作了修改。

另外,CREST采用当前帧最大响应尺度和上一帧尺度加权求和的方法来决定当前帧的最终预测尺度,从而使尺度能够平滑地变化。

LMCF

LMCF借鉴了KCF的循环特征图、Struck的结构化SVM,使用相关滤波在频域加速计算,从而解决了之前结构化SVM系列算法(Struck、DLSSVM)的速度问题。
在前向追踪时,LMCF考虑到画面中相似物体的干扰,提出了一种多峰值的目标跟踪算法(Multimodal Target Tracking),即对高于某一阈值的响应峰值做二次检测,把response map和一个用于筛选的二值矩阵作点乘,相当于把不是峰值的位置滤为0。对于通过筛选的峰值,以每个峰值为中心提取patch并用之前的方法再计算一遍峰值,取此时最大的峰值作为结果。
在模型更新时,LMCF提出了一种高置信度的更新策略(High-confidence Update),由于LMCF主要关注的是实时性,所以希望在算法简单的情况下能够减少失误。在传统的方法中,一般是当最大响应的峰值高于某一个阈值时(认为没跟丢目标),就对模型进行更新;否则若没有响应值超过峰值,就不对模型进行更新。而该工作的实验发现,当目标被遮挡时,响应图会震荡得非常厉害(存在多个较大的峰值),但同时最大响应的峰值仍旧会很高,这就会指导模型进行错误的更新并导致最后跟丢目标。于是作者提出了一个APCE值,定义如下。

只有当最大响应的峰值比较明确,即远超response map中的其他的响应时,APCE值才会比较大。因此LMCF仅当最大峰值和APCE超过阈值时才允许对模型进行参数和尺度模型的更新。

DeepLMCF

同LMCF,不同之处是使用了CNN特征。

MCPF

MCPF结合多任务相关滤波器(MCF)和粒子滤波器,这里的多任务相关滤波器指的是利用了多种特征滤波器之间的相关性。作者对K种特征,定义了参数$z_{k}$去选择具有判别力的训练样本。作者发现,各个特征中的$z_{k}$往往会选择具有相同循环移位的样本,因此不同的$z_{k}$应该具有相似性和一致性。为此,作者在损失函数中增加了矩阵Z的混和范数。
考虑到粒子滤波通过密集采样来覆盖状态空间中的所有状态,这会大大增加计算量,而且并不能保证很好地包括目标物体在一些情况下的状态。因此作者利用MCF对每个采样的粒子进行引导,使其更接近目标的状态分布。这样就可以在提升效果的同时每次采集较少的粒子,从而提高计算效率。另一方面,粒子滤波的密集采样能够在目标尺度发生变化时覆盖状态空间,这就解决了单一相关滤波器的尺度问题。
算法的流程分为四步:
首先,使用转移模型生成粒子并且重采样。
然后,使用MCF对粒子进行微调,使其转移到比较合适的位置。
接着,利用响应更新MCF的参数。
最后,通过求样本均值问题来决定目标的状态,也就是位置等参数。
可见,这里相关滤波仅起到指导的作用,最后的决策由粒子滤波器做出。
顺便一提,MCPF使用了Accelerated Proximal Gradient来解决这里不可微分的凸优化问题(含有范数)。

CFNet

CFNet结合相关滤波的高效性和CNN的判别力,考虑到端到端训练的优势,从理论对相关滤波在CNN中的应用进行了推导,并将相关滤波改写成可微分的神经网络层,将特征提取网络整合到一起以实现端到端优化,从而训练与相关滤波器相匹配的卷积特征。
CFNet采用孪生网络的架构,训练样本(这里指用来匹配的模板)和测试样本(搜索的图像区域)通过一个相同的网络,然后只将训练样本做相关滤波操作,形成一个对变化有鲁棒性的模板。为了抑制边界效应,作者施加了余弦窗并在之后又对训练样本进行了裁剪。
在对比实验中作者发现仅使用一层卷积层时CFNet相比Baseline+CF效果提升最显著,对此作者的解释是可以把相关滤波层理解为测试时的先验知识编码,当获得足够的数据和容量时(增加CNN层数时),这个先验知识就会变得冗余甚至是过度限制。

2018

下面是VOT2018 short-term的排名结果。

STRCF

STRCF(时空正则相关滤波器)主要针对SRDCF的速度做出改进,同时在精度上也有很好的提高。作者发现SRDCF速度很慢的两个原因是:每次对多张图片进行训练打破了循环矩阵的结构,从而无法发挥循环矩阵的计算优势;巨大的线性方程组和Gauss-Seidel迭代法没有闭式解,效率较低。对此,STRCF提出了引入时间正则和ADMM算法。
受online Passive-Aggressive learning的启发,STRCF在SRDCF空间正则的基础上引入了时间正则。我们可以对比两者的回归求解公式具体来看一下。
SRDCF:

STRCF:

这里的$w$表示空间正则化矩阵,越靠近边缘值越大;$f$表示相关滤波器,$f_{t-1}$表示的是$t-1$帧时的滤波器。
忽略每项之前的常数系数,我们可以看到两式的第二项是一样的,也就是STRCF保留了SRDCF的空间正则来抑制边界效应;在第一项中,STRCF没有对过去的每一帧进行求和来训练,这就减小了计算量;同时STRCF加入了第三项时间正则,使得新得到的滤波器与之前的滤波器之间的变化尽可能小,相当于保留了之前的信息。
这么做有两点好处:首先,STRCF可以看作SRDCF的一个合理近似,能很好地发挥后者同样的作用;此外,由于时间正则的引入,使得STRCF不易于在当前帧上过拟合,在遇到遮挡或者超出画面等问题时,STRCF能很好地保持与之前滤波器的相似度从而降低了跟踪器完全跟丢到另一个物体上去的可能,这一定程度上提高了STRCF的精度。

此外,ADMM算法的引入使得最优化求解问题有了闭式解,这比Gauss-Seidel迭代法用稀疏矩阵求解要快得多。得益于SRDCF的凸性,ADMM也能收敛到全局最优点。

UPDT

UPDT区别对待深度特征和浅层特征,主要考虑的是缺少数据和深层卷积在增加语义的同时降低分辨率这两个问题。作者分析了数据增强(flip,rotation,shift,blur,dropout)和鲁棒性训练(也就是tracker应对各种复杂场景和恢复的能力,可以通过扩大正样本的采样范围来训练)对deep feature和shallow feature分别的影响,发现deep feature能通过数据增强来提升效果,同时deep feature主打的是鲁棒性而不是精度;相反,shallow feature经数据增强后反而降低了效果,但同时它能够很好地保证精度。因此,作者得出了深度模型和浅层模型应该独立训练,最后再融合的方案。
作者在文中还定义了Prediction Quality Measure,考虑了精度和鲁棒性,精度用响应分数的锋利程度(sharpness)来体现,鲁棒性则用响应值的幅度来表示,幅度越高表明tracker越确信跟踪的目标,也就是鲁棒性越高。关于具体公式的推导和分析,以及Prediction Quality Measure在预测过程中的具体使用可以看一看原文。

ACT

ACT使用了强化学习,构建了由Actor和Critic组成的学习框架。离线训练时,通过Critic指导Actor进行强化学习;在线跟踪时,使用Actor来定位,Critic进行验证使得tracker更加鲁棒。不同于之前的搜索方案(随机采样或者通过一系列分离的action来定位),ACT希望的是搜索一步到位。这步最优的action也就是离线强化学习所关注的行动,而强化学习的状态由输入到网络中bounding box中框出的图片定义,奖励值根据IoU来定义。
在训练的过程中,由于action space比较大,因此要获得一个正奖励比较困难(随机采取action的话IoU恰好高于阈值的可能性较小)。因此作者利用了第一帧的信息来初始化Actor以适应新的环境。同样的,由于巨大的action space,原本DDPG方法中的噪声引入就不适合跟踪任务了,因此在训练前期,Actor采取的行动以某种概率被一种专家决策所替代。随着训练的进行,Actor越来越强大,这时就逐渐减弱专家决策的指导作用。
在跟踪的初始帧,作者首先在第一帧提供的ground truth周围采集多个样本。然后使用Actor对这些样本作action,根据得分对Actor进行一次微调;对Critic,根据打分和ground truth也进行一次初始化训练。
在之后的跟踪过程中,若Critic的给分大于0,则采用Actor的输出一步到位地预测下一帧的目标;否则,再使用Critic在上一帧周围采集的样本中选出最优作为目标,完成重定向。此外,可以认为Actor在离线训练时已经比较稳定了,因此在跟踪过程中只对Critic进行更新,且仅在Critic给分小于0(认为Critic没能很好地适应目标的变化)时,取前十帧的样本来更新Critic。

DRT

DRT引入了可靠性的概念,考虑到空间正则、掩模等抑制边界效应的方法都不能抑制bounding box内部的背景信息,同时这些方法会导致滤波器的权重倾向于集中在某些较小的区域(主要是中央的关键区域,我理解为边缘区域被抑制掉了,因此学习时自然不会去分配权重),作者认为这是不利于目标跟踪的(容易个别不可靠的区域被误导)。为此,作者提出了DRT,它主要是将滤波器分成了一个base filter和一个reliability term的element-wise product:

这里的base filter用于区分目标和背景;reliability term用于决定每片区域的reliability,由目标区域每一个patch的reliability值加权求和决定:

这里的$p$是对每一个patch的掩模,用于确定每个patch做相关操作的区域;$\beta$有上下界的限定,目的就是为了降低feature map中响应不平衡的影响,防止由于响应的集中而导致仅有一小块区域被关注。

需要最小化的目标方程包括三项:分类误差、局部一致性约束和滤波器参数$h$的二范数。分类误差就是与ground truth之间的损失函数,计算时需考虑可靠性;局部一致性约束用于减小循环样本中的每一个片段的响应差距,该项不受$\beta$即可靠性的影响,也就是说base filter在训练时依旧要保持对每个局部区域同样的关注度,使得base filter能独立于可靠性进行训练,这就避免了前面提到的滤波器在训练时边缘区域被抑制所造成权重集中的后果;滤波器参数$h$的二范数用于保证模型的简单程度,防止模型退化(可以理解为过拟合)。
由于只有当base filter的参数$h$和reliability的权重$\beta$有一项已知时,目标方程的最小化问题才是凸优化问题,因此作者采用了$h$、$\beta$交替训练的方法。
作者还使用权重逐帧退化的方式设计了一种简单的利用多帧信息的目标方程。借鉴ECO,DRT也采用了间隔几帧更新一次的稀疏更新方法和基于高斯混合模型的样本分组策略。类似DSST,DRT采用了先确定位置再计算多个尺度的响应的“两步”尺度估计方法。

MCCT

MCCT使用了多特征集成学习,在跟踪时对每一帧分别选用最合适的特征来做出决策。为了应对不同的场景,MCCT选择了low,middle,high三个层级的特征,并通过排列组合得出7种expert。尽管有些特征的鲁棒性明显差于三类特征的组合,但是它们提供的多样性对集成学习是至关重要的。

为了评估每个expert在每一帧的好坏以决定具体选用哪一个,作者提出了Expert Pair-Evaluation和Expert Self-Evaluation。
Expert Pair-Evaluation分为两项:在第一项中,作者认为一个expert的好坏可以通过它与其他expert的整体一致性来体现,于是首先计算了每个expert相对于其他6个expert在当前帧预测结果的一致性(通过重叠率来衡量)之和;此外,作者认为一个好的expert还必须是temporal stable的,因此他又计算了每个expert相对于其他6个expert在前几帧内预测趋势的一致性,这就可以防止因为在当前帧碰巧预测一致而导致之前一项的分值很好的情况,也保证了expert的可信度。最后两项结合得到Expert Pair-Evaluation。
在Expert Self-Evaluation中,作者认为路径的顺滑程度一定程度上能够体现每个expert的可靠程度。
最后将Expert Pair-Evaluation和Expert Self-Evaluation加权求和选出每帧最好的expert做出决策。
MCCT提出了一种peak-to-sidelobe ratio和鲁棒性的置信度分数来进行模型更新:

其中,$P_{mean}^{t}$是每个expert响应图peak-to-sidelobe ratio的平均,$R_{mean}^{t}$亦然。当$R_{mean}^{t}$比较低时,认为采集到了不可靠的样本(比如遮挡问题等)。为此,作者的模型更新策略是,当置信度分数$S^{t}$大于之前置信度均值时,采用正常学习率,否则,根据置信度算出一个较小的学习率以在一定程度上维持模型。
为了提升速度,每个expert之间共享了样本和RoI,最后MCCT的速度为7.8FPS,MCCT-H(没采用深度特征)的速度为44.8FPS。(作为参考,ECO的速度为15FPS)

LSART

LSART分析了深度特征中的空间信息,提出了两种互补的回归方式来使得跟踪更加鲁棒。
作者首先对比了CNN-based和KRR-based(核岭回归)两类tracker,认为它们各有利弊且是互补的。由于KRR的循环采样,目标的结构特征会被打破,对形变和遮挡问题效果不好,而CNN则能够很好地提取位置信息;相反,CNN庞大的参数量使得它容易过拟合,而KRR-based tracker就不会出现这样的问题。因此,若将两者结合(将热力图加权求和),就可以让KRR关注全局而让CNN关注较小、较精确的目标,进而达到更好的效果。
对于KRR,作者引入cross-patch similarity,将参数看作训练样本的加权求和,将响应项拆分成三个模块,这就方便把原本的迭代求解的方式分成三步在神经网络中来求解了。
对于CNN,考虑到形变和遮挡等问题会使得目标的一部分比其他区域更加重要,不同于以往在feature map上做文章,作者对卷积层的滤波器施加掩模,使得各个滤波器关注于不同的区域,在跟踪的过程中,这些掩模不做变化。此外,作者还提出了距离变换池化层用于评判输入feature map的可靠性。另外,作者设计了一种two-stream的训练网络,将空间正则的卷积层和距离变换池化层分开训练以防止过拟合,能够比较好的处理旋转问题。

SiamRPN

SiamRPN利用了Faster RCNN中的RPN,解决了之前深度学习跟踪算法没有domain specific(可理解为类间不区分)以及还需额外的尺度检测与在线微调的问题。RPN回归网络的引入,一方面提高了精度,另一方面回归过程代替多尺度检测,使得速度有所提升。
在在线跟踪时,SiamRPN将跟踪看作one-shot检测的问题,也就是用第一帧目标样本的信息来预测RPN网络中的参数,从而实现domain specific且不需要在线更新。作者把template分支和detection分支卷积视作类别信息在RPN网络上的embedding。

DaSiamRPN

DaSiamRPN在之前的孪生网络系列的基础上增加了distractor-aware,这里的distractor指的是在判别式方法中,不同于无语义信息易判别的背景,而存在一定的语义并对前景分割存在干扰的背景。这其中的一大原因是之前的训练集仅从同一个视频序列的不同帧中采样,造成了non-semantic的背景样本具有较大的比重而semantic的背景样本较少,这就弱化了模型准确判别前景的能力。此外,之前的孪生网络系列还存在不能在线更新和不进行全局搜索这两个问题。
首先,作者提出了三类样本选取方法来弥补传统采样的不足。考虑到视频数据集中类别缺乏和标注的难度,作者引入了ImageNet和COCO图像检测两个数据集,并把样本分成三类对tracker进行训练。

对于正样本对,其作用是提升tracker的泛化能力和回归精度;对于来自同一类别的样本对,其作用是让tracker更注重细粒度的表达方式,提升判别能力;对于来自不同类别的样本对,其作用是让tracker在遮挡、超出视野等情况下拥有更好的鲁棒性。
值得一提,作者发现motion pattern能很好地被浅层网络建模,因此在数据增强时还引入了运动模糊。
DaSiamRPN通过上述方法对数据做了增强,可是在跟踪特定目标时,还是很难将一般模型转化为特定视频域所用。考虑到上下文信息和时域信息可以提供特定目标的信息以增加tracker的判别能力,作者提出了一个distractor-aware module。具体来说,在上一帧中选择出的proposal中,通过非极大抑制处理,剩下的proposal中最大的就是目标,剩下的就是会产生误导的distractor;在当前帧,为了抑制这些distractor的干扰,可以减去这些distractor之前响应的加权和,减去之后还是最大的proposal就是我们要找的目标,其基本思想如下公式所示:

这里的$\alpha$是控制distractor影响大小的权重系数,作者又对上式进行调整,通过引入学习率使得该分类器在线可学习,这就无需利用反向传播更新网络参数,而通过微调一个分类器弥补了传统基于孪生网络的tracker不能在线更新的缺点。
此外,当认为目标跟丢时,DaSiamRPN会匀速扩大搜索范围,并且通过高效的bounding box回归来代替图像金字塔,这就通过一个简单的方法在应对长时跟踪目标消失问题时较之前基于孪生网络的tracker取得了一个进步。

Meta-Tracker

Meta-Tracker将元学习运用在了目标模型的初始化上。作者认为结合深度特征和在线学习的模型有两大困难,一是训练的样本不容易获得,二是大多数SOTA的tracker在训练阶段都需要花费大量的时间在初始化上面。
针对上面的难题,作者提出了一种在未来的帧上训练目标模型的思路,采用了基于预测梯度的策略学习方法获得普适性的初始化模型,使得跟踪模型自适应于后续帧特征的最佳梯度方向,从而在接收到第一帧时仅需一步迭代就能使参数快速收敛到合适的位置。这样做有三点好处,一是能使模型更加关注对后续的帧更有价值的特征,二是避免了在当前帧上过拟合,三是能够使初始化更快速。总而言之,就是能保证精度和鲁棒性。
考虑到上述方法在长序列或者目标在帧与帧之间变化不大时表现不佳(会偏离目标),这是因为Meta-Tracker一步到位的思想使得学习率会偏大。因此作者仅在模型初始化时采用学习到的学习率,在之后的跟踪过程中仍旧沿用原来版本的方式进行更新。这里原来的版本指的是CREST和MDNet,作者在这两个tracker的基础上改进出了MetaCREST和MetaSDNet。具体的改进和处理可以看一看我之前写过关于Meta-Tracker的文章。

DorT

DorT(Detect or Track)把跟踪看作一个连续决策的过程,它结合目标检测和目标跟踪两个领域内SOTA的结果,在孪生网络输出的结果上再添加一个小型的CNN网络作为scheduler来判断在下一帧是作检测还是作跟踪。

LADCF

LADCF针对DCF系列的边界效应和模型退化(后者主要是单帧独立学习和模型更新速率固定导致)的问题,提出了一种空间域特征选择和时间域约束结合的方法,并且使其能在低维流形中有效表示。

补充:流形学习的观点认为,我们所能观察到的数据实际上是由一个低维流形映射到高维空间上的。由于数据内部特征的限制,一些高维中的数据会产生维度上的冗余,实际上只需要比较低的维度就能唯一地表示。

掩模策略应用于目标跟踪时,仅将目标区域的参数激活。LADCF也运用了这个思想,对滤波器中的参数$\theta$作降维处理$\theta _{\phi }=diag(\phi )\theta$,这里$\phi$中的元素要么是0、要么是1,即不激活或者激活。不同于PCA和LLE,这种方法在降维的同时也保持了空间特性,不仅能加速求解,也能除去大部分干扰,使滤波器关注于目标部分从而可以使用更大的搜索域。
最后的目标函数如下:

可以看到,这里还包括与历史模型的正则项,减轻了滤波器退化。作者让$\lambda _{1}< < \lambda _{2}$,也就是让时间域上的一致性更加重要于特征的稀疏选取。

FlowTrack

不同于之前先把光流算好,FlowTrack是第一个把光流信息进行端到端训练的,这无疑提高了光流使用的精度。作者注意到之前的算法大都采用RGB特征也就是外观特征,且缺少对运动特征和帧与帧之间联系的利用,这就导致在部分遮挡和形变等情况下效果会变差。不同于把之前的几帧保留下来等时域方法,作者希望能把前几帧的特征直接补充到当前帧上融合成一个,而具备方向和速度信息的光流就成了一种比较合适的方法。为此,作者将光流引入孪生网络框架,使得仅使用外观特征的一些不足得到弥补。
作者的思想很巧妙,我以遮挡问题为例简述一下。作者的想法是当当前帧的目标有部分被遮挡时,我们可以根据光流的运动信息,把前几帧的特征映射过来对齐,通过插值补全当前帧的特征,简单来说可以理解为把当前帧缺的那块给补全。为了有效地选择补过来的特征,作者使用了空间注意力和时间注意力两种机制结合。空间注意力分别计算前几帧补过来的特征与当前帧的特征的相似度,根据相似度大小来分配特征在各个空间位置上的权重。但是由于最近的一帧总是与当前帧的特征最为相似,它的权重肯定是最大的,考虑到假如最近一帧由于遮挡等原因等导致特征质量下降而不适合分配最大的权重,因此还需要时间注意力来重新校准权重。具体来说,时间注意力作用在空间注意力的输出上,对于一般情况下的目标基本不会改变空间注意力的输出,而对遮挡等情况下的帧就会减小其在空间注意力中分配到的权重。

VITAL

VITAL针对正样本高度重叠无法捕获目标丰富的特征变化和正负样本不平衡的问题,采用对抗学习的思想,分别设计了生成网络和代价敏感损失来解决这两个问题。
这里的生成网络输出为一个作用于目标特征的掩模。通过对抗学习,该生成网络可以产生能保留目标特征中较为鲁棒的部分。其目的是对仅在个别帧判别力强的特征进行削弱,防止判别器过拟合于某个样本。个人理解,这里的判别力强的特征并不是指的能够很好地区分目标和背景的特征,而是指仅在某些帧出现比较独特,而在大多数情况下不存在于目标上的特征。
考虑到很容易被分类正确的负样本在训练过程中也会产生损失,然而我们并不希望网络关注这些损失,因为关注他们反而会使得网络性能变差。因此,为了避免很多负样本主导损失函数,作者采用高阶敏感损失,减小简单负样本的权重,这不但提升了精度,也加速了网络的收敛。

SA-Siam

SA-Siam考虑到语义信息和外观信息的互补关系,构建了两个并行的孪生网络。在外观网络(作者使用的是SiamFC)具有判别力的基础上,结合语义网络(作者使用了AlexNet的第4和第5层作为backbone)更泛化、更鲁棒的语义信息,使得tracker的效果更好。
外观网络的输入为目标和搜索区域,其框架跟SiamFC基本一致,这里就不细说了。
不同于外观网络,语义网络的输入为目标及其背景和搜索区域(两者一样大),作者直接使用了AlexNet提取高层的语义信息并不进行训练,因为假如训练了的话就跟外观网络接近了,而集成学习的思想就是被集成的各个模型之间的关联性应比较弱,这也是文中多次强调的。在提取高维语义信息之后,为了使得得到的语义特征更适合于跟踪任务,作者使用了1x1的卷积层作了一次特征的fusion。此外,考虑到对不同的追踪目标各个通道的重要性是不同的,作者还设计了一个通道注意力模块,具体而言,就是对每一个通道,先做最大池化变成3x3的网格,然后再通过一个多层感知机来决定每个通道的每个格子的重要性。由于通道注意力的选择也受目标周围背景的影响,因此最大池化的结果仅中间的格子是目标区域,周围的8个格子代表目标周围的背景信息。再次强调,在训练过程中,语义网络冻结语义特征提取的AlexNet,仅训练特征fusion和通道注意力模块。
根据上文的解释,同样的,为了保持语义网络和外观网络的弱关联性和互补性,两支网络独立训练,仅在测试时加权得到最后的相似度图作为预测依据。

2019

下面是VOT2019 short-term的排名结果。

GFS-DCF

GFS-DCF考虑到深度网络的高维通道存在许多冗余的信息,因此作者在时域和空间域之外,还考虑了通道维度的影响,在目标函数中使用了三个正则项。

注:建议结合上图来看接下来的分析。

对于空间域,作者将每个通道(也就是每个feature map)的对应点相连接,用范数约束,可以理解为提取那些在绝大多数特征图中都是最重要的特征的位置。

从通道角度,作者又把每一个通道作为一项来做约束,可以理解为提取那些特征比较重要的通道。

对于时域,作者使用了low-rank约束,这里的rank指的是矩阵的秩而不是排名,low-rank主要用于图像对齐(alignment),在文中的目标是最小化$rank(W_{t})-rank(W_{t-1})$,这里的$W_{t}$表示从1到t每个滤波器向量化后形成的矩阵。下面是作者最后修改后的正则项:

注:上述三项在实际目标函数中还要加上权重。

作者发现,空间正则对使用handcrafted特征的模型效果显著,而对使用CNN的模型(文中是ResNet)效果提升不大;相反,通道正则对使用handcrafted特征的模型效果不明显,而对使用CNN的模型效果显著。作者在文中解释认为由于深层CNN特征表示的语义信息丰富而缺少细粒度的信息,因此相比保留更多空间结构handcrafted特征,对深层CNN特征使用空间正则比较难以判别哪些位置的特征反应了目标位置的信息。此外,由于在训练过程中一些通道的权重下降到很小,也就是说模型本身就不怎么关注这些通道,因此使用通道正则在这里取得了比较明显的效果。

D3S

D3S考虑到BBox对目标的粗糙表示会影响性能以及视频分割任务中对背景干扰和长时视频不鲁棒的问题,提出了一种视频跟踪、视频分割互补的框架。

如上图所示,作者构造了用于分割的GIM和用于定位的GEM两个模块。
GIM使用初始帧的目标像素点构造目标的特征向量,使用初始帧目标周围的像素点构造背景的特征向量。在之后的图像中,每个像素点的目标相似度,定义为该点和每个目标的特征向量做相似度计算之后,最大的K个目标相似度的均值;同理,每个像素点的背景相似度,定义为该点和每个背景的特征向量做相似度计算之后,最大的K个背景相似度的均值。最后将目标相似度图和背景相似度图作softmax得到分割结果。该模块没考虑位置信息,用分割使得在应对剧烈形变的目标时能够取得较好的效果。
然而,GIM的分割结果对相似的物体或者背景的干扰并不鲁棒,因此我们希望能有鲁棒的位置信息来提升判别力。GEM就是直接使用一个DCF来找到最大响应值的位置,也就是目标的中心位置,然后以该中心为圆心,向周围每个像素点根据半径由大到小分配置信度。最后仅把GIM中得到的且由GEM分配的置信度高于阈值的目标像素点作为分割结果。
由于通过backbone的encode导致此时的输出分辨率较低,因此还需要作上采样。具体来讲,就是每次把输入扩大到两倍分辨率,通过两次卷积,然后加上backbone中对应分辨率的层。
由于预训练的backbone特征缺少精细的划分,因此作者在初始帧先进行降维的训练。具体来讲,就是首先将预训练网络通过1x1的卷积来降维,再通过一层3x3的卷积,从而达到调整网络参数使得分支划分得到最佳的目的。
由于输出是二值掩模,因此作者还对使用BBox的目标跟踪问题作了变换处理。首先,在初始帧,除了进行降维和DCF的训练,作者还先把BBox内部的像素点视为目标点,把BBox外4倍区域的像素点视为背景点,用D3S在第一帧上迭代(文中说只迭代1次就可以了)以产生比BBox更细致的分割,将最后分割出的目标点和其周围的背景点用于构造特征向量,在之后的跟踪过程中保持不变并用于GIM模块。此外,在输出时,作者先用椭圆去近似掩模,然后根据长轴和短轴来确定BBox。由于椭圆的确定遗漏的背景信息(仅考虑怎么把mask包进来)从而导致BBox偏大,因此作者提出了一种考虑背景信息的方法,对长轴进行微调来确定最终的BBox。

GlobalTrack

GlobalTrack关注的是long-term tracking的问题。我们知道,在目标跟踪问题中,为了更好的利用前一帧甚至前几帧的信息,往往会对模型做很多假设,包括目标的运动、位置变化、尺度变化(假设平滑变化等等),而这些假设并不能很好地处理所有的情况(比如位置或尺度突变、目标消失、短时跟踪失败等),由此产生了模型的累计误差。而在长时跟踪问题中,这样的累计误差往往会使得后期的目标跟踪结果差很多。
基于上述考虑,GlobalTrack根据长时跟踪的特点,把跟踪看作在每一帧作全局检测的问题,设计了一种没有运动模型、没有在线学习、没有位置估计、没有尺度平滑的无累积误差的baseline,其基本框架如下图所示。

受Faster RCNN启发,GlobalTrack也是基于two-stage的框架。其下面的一条和Faster RCNN基本一致,由于Faster RCNN是目标检测类算法,其目的是在图像中框出所有物体并分类。而目标跟踪仅需要目标,因此作者用添加了上面一条query-specific的引导。为了简单起见,作者把query的RoI看成kxk的方型特征,在第一个feature modulation中,作者把RoI特征卷积成1x1的卷积核,然后再和通过backbone的搜索图像特征做卷积相关操作,得到query-specific的候选框;在第二个feature modulation,作者把RoI特征与每个候选框作哈达玛积(也就是简单地将两个尺寸相同的矩阵的对应位置作乘积),由此来改进标签置信度和BBox的预测。
在训练时,作者取多对图像对,其中每一对图像都共同含有M个相同的实例,作者用每对图像来相互预测每对各自的M个实例,使总的损失达到最小以进行训练。
在测试时,作者简单地把第一帧作为query,之后的每一帧都视为独立的全局检测,直接取得分最高的BBox作为结果,如此就不存在依赖相邻帧带来的累计误差了,因此作者认为视频长度越长,GlobalTrack的表现就越突出。

SPSTracker

SPSTracker针对目标周围噪声引起的响应(sub-peak)导致模型漂移,以及由多尺度样本加权得到的特征图响应最大值和目标真正的几何中心不一致的问题,提出了BRT和PRP两个模块。作者使用的是ATOM的框架,两个模块在框架中的使用方式如下图所示。

这里的BRT其实就是简单的将远离目标中心的点置为0,其目的是减小响应图的方差,使得响应图尽可能地呈现单峰响应的形状,此外也起到了抑制边界效应的效果。PRP其实就是作者新设计的一个池化层,该池化层把每一像素点的值设为该像素点所在列所有像素点中的最大值和所在行所有像素点的最大值之和,从而使响应值能更加靠近目标的几何中心,从而能够更好地使用多尺度样本。作者对搜索区域使用BRT,然后将通过分类器的置信度图通过一个PRP构造的残差模块,从而达到抑制sub-peak的目的。

SiamRCNN

SiamRCNN发现重检测很容易受到干扰物的影响从而产生模型漂移,从难例挖掘和运动轨迹动态规划两个角度入手,设计了一个利用第一帧和前一帧为模板的孪生网络检测结构,在短时跟踪评价上能与之前SOTA的算法持平,在长时跟踪评价上有非常显著的进步。注意,这里的追踪轨迹不仅追踪目标,同时也追踪所有的distractor。

SiamRCNN可以说综合了许多前人的成果。为了收集更多难例,作者在不同的视频上根据嵌入网络上的距离来获得更多负样本。接下来,我结合上面的网络结构图来简述一下我对该算法的理解。
首先,作者使用RPN网络获得了类别无关的proposal,然后和第一帧结合,通过一个与Faster RCNN前半段相似的Re-Detection Head。接着,通过3个级联的RCNN获得回归后的bounding box(这里借鉴了Cascade RCNN,其认为高质量的proposal能够产生更好的结果,3级RCNN的IoU阈值设定逐级升高)。随后,作者将此时获得的bounding box与前一帧所有的检测结果两两组合(也就是即检测目标也检测干扰物),再次输入到之前相同的重检测结构中。最后对得到的所有结果进行跟踪轨迹动态规划。
在轨迹动态规划模块,作者计算轨迹与检测结果的相似度,用不具有不确定性的检测结果去延续之前的若干条轨迹。这里的不确定性定义为:对这条跟踪轨迹,有其他的检测结果和本结果具有相当的相似度;对检测结果,有其他的跟踪轨迹和本轨迹具有相当的相似度。对于不确定的检测结果,作者让它们开启新的轨迹。对于没有确定检测结果的轨迹则暂时中断,其中也包括含有初始帧的轨迹,当它中断时可认为目标消失了。


研究趋势

以下是我对近几年来目标跟踪领域各种算法主流的研究趋势和发展方向的一个浅析,个人思考,多多指教。

注:其实近几年还出现了一些其他的关注方向,由于不是主流、目前关注较少、本人学识不够等原因,在此不做列举。

信息提取

深度特征

早期的目标跟踪算法主要在handcrafted特征方面进行探索和改进,以2012年AlexNet问世为节点,深度特征开始被引入目标跟踪领域。
我们知道,在现实场景中,物体是在三维的运动场中移动的。而视频或图像序列都是二维的信息,这其实是一些难题的根本原因之一。一个比较极端的例子就是理发店门前经常会出现的旋转柱,如果单纯地从二维角度来看,柱子是向上运动的,可在实际的运动场中柱子是横向运动的,观测和实际的运动方向是完全垂直的。

因此,为了能够更好地跟踪目标,我们需要提取尽可能好的特征,此外最好能从视频或图像序列中学到更多丰富的信息(尤其是含语义的)。值得注意的一点是,在港科大王乃岩博士2015年所做的ablation experiments中(详见参考文献[2]),发现特征提取是影响tracker效果最重要的因素。
考虑到精度是保证目标跟踪鲁棒性的重要因素,不同于一些其他的计算机视觉任务,目标跟踪领域的深度算法比较强调结合与充分利用浅层网络的高分辨率信息。

时域和空间域结合

可以说,在目标跟踪的相关算法中,空间正则指的就是抑制边界效应。由于CNN能够在学习的过程中能够产生对样本中各个区域有区分的关注度,因此可以不考虑边界效应。对边界效应的处理主要是在相关滤波类等需要循环移位的算法中出现。
事实上,目标跟踪这一个任务本身就在利用时域信息,因为预测下一帧肯定需要上一帧的信息,然而仅仅利用上一帧的信息往往是不够的,充分的利用时域信息在正则或者辅助记忆方面都可以取得一定的效果。

学习方式

结合语义分割的多任务学习

由于bounding box粗糙的对目标的标注表示使得有不少冗余的背景信息进入template,此外bounding box对平面内旋转等场景不鲁棒,这些都会导致模型的退化。早期也有许多算法考虑到了分割,但主要是对目标进行分块(part-based)而不是语义分割。由于跟踪的目标是有具体形状的且是一起运动的,同时判别式方法正是要分离除目标以外的物体,因此近年来有不少算法结合语义分割,通过多任务学习来进一步提高tracker的效果。具体来说,就是引入掩模(mask)来识别预测物体,最后在转化成bounding box作为结果。

元学习

实际上,目标跟踪这一个任务本身的特性就决定了它与元学习的思想有共通之处。元学习主要针对的是两个问题:在少样本学习的情况下对样本的利用效率比较低;当进行一个新的任务时对之前学到的经验的可移植性差,我觉得这里的新任务可以指从分类到跟踪这样类别之间的转换,也可以指在不同的视频序列上训练和测试这样“域”之间的转换。
当深度特征兴起之后,目标跟踪中的许多算法都选择迁移目标分类任务中的一些预训练模型来提取特征,这种迁移学习其实就包含了元学习的思想。MDNet将每个视频看做一个域,在测试时新建一个域但同时保留了之前训练时在其他域上学到的经验,既能够更快更好地在新的视频序列上学习也避免了过拟合。孪生网络实际上也是元学习领域一种比较常用的结构,它学习了如何去学习输入之间的相似度。

其他关注点

样本采集

样本采集主要包括样本的数量、样本的有效性、正负样本和难易样本的平衡性。

防止过拟合

每帧获取的少量信息和目标的意外变化导致信息的丢失,使得过拟合问题成为目标跟踪任务中一个比较重要的关注点,下面是一些比较常见的方法:

  • 冻结一些层的参数(设置学习率为0),仅更新一部分层的权重。
  • 采用coarse-to-fine的网络框架,用语义信息来避免过拟合。
  • 通过在目标方程中添加正则项来限制模型的稀疏度,也即参数量。
  • 采用two-stream的模型结构,短时更新和长时更新相结合。
  • 采用稀疏更新的方式(隔几帧更新一次),相当于将利用单帧信息的更新变成了批处理的形式。
  • 每次更新采用最近几帧的信息而不是只用目前帧的信息,其原理类似上一条。
  • 利用初始帧或者质量比较好的几帧存储的样本来进行时域正则。
  • 对不同的情况采用不同的更新或者初始化的策略。
  • 使用dropout来防止过拟合。
  • 使用基于patch的跟踪算法。
  • 使用掩模去除不可靠的信息。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:目标跟踪的小总结

文章作者:高深远

发布时间:2020年02月15日 - 21:42

最后更新:2020年04月18日 - 13:00

原始链接:https://gsy00517.github.io/computer-vision20200215214240/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
computer vision笔记:目标跟踪的小总结 | 高深远的博客

computer vision笔记:目标跟踪的小总结

看了一寒假的目标跟踪,一直想将自己学习到的内容整理归纳一下,迟迟没动笔(其实是打字,但说迟迟没打字比较难听哈哈)。如今快要开学了,决定还是积累一下。
本文主要是把目标跟踪中的一些相关要点做一个总结,并且按具体问题对一些主流的或者我阅读过觉得有价值的算法做一个简单概述。近年来各类方法层出不穷,且码字不易,无法涵盖所有的方法。此外,由于包含自己的转述和理解可能会存在错误。在今后的学习过程中会一直保持本文的更新,因此这将是一篇LTS的文章哈哈。

注:本文重点关注单目标跟踪。

References

参考文献:
[1]统计学习方法(第2版)
[2]Understanding and Diagnosing Visual Tracking Systems
[3]Survey of Visual Object Tracking Algorithms Based on Deep Learning
[4]Handcrafted and Deep Trackers: Recent Visual Object Tracking Approaches and Trends
[5]Review of visual object tracking technology
[6]A Review of Visual Trackers and Analysis of its Application to Mobile Robot
[7]Deep Learning for Visual Tracking: A Comprehensive Survey
[8]Video Object Segmentation and Tracking: A Survey
[9]Object Tracking Benchmark
[10]The Visual Object Tracking VOT2013 challenge results
[11]The Visual Object Tracking VOT2014 challenge results
[12]The Visual Object Tracking VOT2015 challenge results
[13]The Visual Object Tracking VOT2016 challenge results
[14]The Visual Object Tracking VOT2017 challenge results
[15]The sixth Visual Object Tracking VOT2018 challenge results
[16]The Seventh Visual Object Tracking VOT2019 Challenge Results

注:参考文献重新整理中,待补全…


简介与要求

目标跟踪是利用一个视频或图像序列的上下文信息,对目标的外观和运动信息进行建模,从而对目标运动状态进行预测并标定目标位置的一种技术。一般是在第一帧给出一个框,框中的物体就是我们需要在后续帧中用算法进行跟踪的对象。就目前的单目标跟踪而言,一般有如下要求:
monocular:我们的视频或者图片序列是仅从一个摄像头中获得的,也就是不考虑比如在城市道路场景中跨摄像头对目标跟踪的复杂应用。
model-free:没有任何先验,也就是在获取第一帧的框之前我们并不知道会框出什么物体,也不需要在之前对初始框中的物体进行建模。
single-target:只追踪第一帧框出的那一个物体,也就是除了那个物体之外所有的物体都是back ground。
casual/real-time:目标跟踪是一个在线过程,也就是不能提前获取未来的框对目标进行跟踪。
short-term:没有重检测,也就是目标跟丢了就丢了。
long-term:可以在跟丢之后重检测,这类算法一般除了跟踪之外还需要有检测的功能。

下面是目标跟踪流程的伪代码表示(不一定普适,比如有些算法不在线更新,但符合基本的过程)。


问题及挑战

通俗来讲,目标跟踪的最终目标就是要又快又准。“快”主要表现在计算量小和所需的存储空间小,“准”就是预测出的bounding box要尽可能地接近ground truth。除了上面两个基本需求(也可以说是为了更好地达到这两个基本需求),近年来的算法主要针对目标跟踪中的一些挑战进行突破,从而更好地解决某些问题之后达到更好的整体效果。
总的来说,目标跟踪的主要问题有如下这些:遮挡(occlusion)、背景干扰(background clutter)、光照变化(illumination changes)、尺度变化(scale variation)、低分辨率(low resolution)、快速移动(fast motion)、超出画面(out of view)、运动模糊(motion blur)、形变(deformation)、旋转(rotation)等。

OTB数据集依据各种问题对其中的序列进行了一个划分,这对之后针对性的研究提供了重要的参考。


生成式与判别式

利用特征判断候选样本是否为跟踪目标,可将目标跟踪的模型分为生成式模型和判别式模型,本小节就介绍一下什么是生成式模型和判别式模型。

机器学习

我们首先看看在机器学习中生成式模型和判别式模型定义的一般区分。
一般而言,机器学习的任务就是学习一个模型,应用这一个模型,对给定的输入预测相应的输出。输出的一般形式可以是决策函数,也可以是条件概率分布。
对于生成式模型,我们需要通过数据学习输入X与输出Y之间的生成关系(比如联合概率分布),也就是认为X和Y都是随机变量。典型的生成式模型有朴素贝叶斯模型、隐马尔可夫模型(HMM)、高斯混合模型(GMM)等。
对于判别式模型,我们只需要直接学习决策函数或者条件概率分布,只关心对给定的输入X我们需要输出怎么样的Y,也就是不考虑X是否是随机变量。典型的判别式模型包括k近邻、感知机、决策树、逻辑斯蒂回归模型、最大熵模型、支持向量机(SVM)、提升方法和条件随机场等。此外神经网络也属于判别式模型。
相较而言,生成式模型体现了更多的信息,不过这还是因条件而异的,不同情况不同任务两种方法各有优缺点。

目标跟踪

在目标跟踪领域,生成式模型通过提取目标特征来构建表观模型,然后在图像中搜索与模型最匹配的区域作为跟踪结果。不论采用全局特征还是局部特征,生成式模型的本质是在目标表示的高维空间中,找到与目标模型最相邻的候选目标作为当前估计。此类方法的缺陷在于只关注目标信息,而忽略了背景信息。

与生成式模型不同的是,判别式模型同时考虑了目标和背景信息。它将跟踪问题看做二分类或者回归问题,其目的是寻找一个判别函数,将目标从背景中分离出来,从而实现对目标的跟踪。

一般来说,在目标跟踪领域,判别式充分利用了目标前景和背景信息,能更加有效地区分出目标,比单单运用目标区域特征进行模板匹配的生成式模型在复杂环境中的鲁棒性更强。


算法导图

首先是参考文献[6]中的一个树状导图。

下图是中科院博士王强(github名为foolwood…呃不得不说这名字取得真谦虚)在github上上总结的Benchmark Results中的一个思维导图,同一个链接下还包括了各项成果的paper及code,值得收藏一下。

补充:这里再推荐一个在github上维护的Tracking Benchmark for Correlation Filters,按每篇论文针对或者解决的问题来分类,比较清楚,可以收藏一下。但这个仓库似乎在2017年后就没有更新了,可能是深度学习的进入或者说相关滤波系列和深度学习融合使得独立的相关滤波算法不那么突出了。

下图是浙大硕士王蒙蒙极市平台做分享的时候所用的一张思维导图,归纳得也比较清晰。

注:后两张导图中都把历年benchmark的冠军工作作了标注。

对比几张思维导图可以发现,他们都把主流算法分成了相关滤波、深度学习两个分支(或者说是基于handcrafted特征的算法和基于CNN提取特征的算法,其实近年已有所融合),此外还有一些基于强化学习、结构化SVM的模型。其实,目标跟踪算得上是计算机视觉领域中深度学习涉足较晚的一个方向,其主要原因是目标跟踪相关数据集的标注花费较大。此外,相关滤波的速度优势,也就是实时性是十分引人注目的,但在应付当前目标跟踪中的各种挑战、问题时,相关滤波的鲁棒性还是落后于深度学习方法的。
在下一节,我将结合上面几张导图,对历年尤其是近几年的算法做一个简单的整理,以方便日后的学习与研究。


各类算法的梳理与简述

本节按年份顺序对各个算法进行一个简单地梳理,其中各个算法的年份以论文发表的年份或者参加benchmark的年份为依据,可能会存在1年的区别,但影响不大。其中各年的各个算法根据算法的效果和影响大致上呈递减排序。对2013以后的算法,我拷贝了VOT challenge的结果排名,以供参照。

注意:如果你对计算机视觉或者说目标跟踪方面的一些基础方法、概念和经典算法已经有些了解,可以跳过本条建议。
考虑到在后文频繁地插入链接不太好,我就在此先推荐一下我博客的几个标签目标跟踪计算机视觉深度学习机器学习以及线性代数,其中的文章包含了一部分接下来要提到的概念和算法,可以事先浏览一下。当你在阅读时对相关概念、方法感到迷惑或者想进一步了解,博客内置的搜索功能或许能够为你提供帮助。

1981

LK Tracker

LK Tracker应该是最早的目标跟踪工作,它使用了光流的概念,如下图所示,不同颜色表示光流不同的方向,颜色的深浅表示运动的速度。

LK Tracker假定目标灰度在短时间内保持不变,同时目标邻域内的速度向量场变化缓慢。由于光流方程包含坐标x,y和时间t共三个未知数,其中时间变化dt已知而坐标变化dx和dy未知,一个方程两个未知数无法求解,因此作者假定相邻的点它们的光流具有空间一致性,即实际场景中邻近的点投影到图像上也是邻近点,且邻近点速度一致,这样就可以求解方程组了。下图是求解之后的光流向量,其中绿色箭头的方向表示运动方向,线段长度表示运动速度的大小。

光流的计算非常简单也非常快,而且由于提出得很早,各种库都有实现好的轮子可以轻松调用,但是它的鲁棒性不好,基本上只能对平移且外观不变的物体进行跟踪。

1994

KLT

KLT是一种生成式方法,也是使用了光流特征。在此基础上,作者使用了匹配角点的方法,也就是寻找边角处、纹理处等易辨识的地方计算光流来进行追踪。

1998

Condensation

Condensation(Conditional density propagation)条件密度传播使用了原始的外观作为主要特征来描述目标,采用了粒子滤波,这是一种非参数化滤波方法,属于生成式模型。它定义了一个粒子样本集,该样本集描述了每个粒子的坐标、运动速度、高和宽、尺度变化等状态;此外,通过一个状态转移矩阵和噪声定义系统状态方程。基于蒙特卡洛方法,粒子滤波将贝叶斯滤波方法中的积分运算转化为粒子采样求样本均值问题,通过对状态空间的粒子的随机采样来近似求解后验概率。

2002

Mean Shift

Mean Shift采用均值漂移作为搜索策略,这是一种无参概率估计方法,该方法利用图像特征直方图构造空间平滑的概率密度函数,通过沿着概率密度函数的梯度方向迭代,搜索函数局部最大值。在当时成为了常用的视觉跟踪系统的目标搜索方法,简单易实现,但鲁棒性较低。

2003

Feature Selection

Feature Selection利用线性判别分析自适应地选择对当前背景和目标最具鉴别性的颜色特征,从而分离出目标。

2006

Boosting

Boosting结合Haar特征和在线Boosting算法对目标进行跟踪。Boosting算法的基本思路就是首先均匀地初始化训练集中各个样本的权重,然后初始化N个弱分类器,通过训练集进行训练。第一次训练时,对第一个弱分类器,通过它在训练集上的错误率确定它的权重,同时更新训练集的样本权重(增加分类错误的样本的权重),然后,用新的训练集训练第二个弱分类器,计算它的权重并更新训练集的权重。如此迭代,将得到的分类器与它们的权重相乘,累加起来便得到一个强分类器。
上面所述是针对离线训练的,当在线训练(比如跟踪)时,为了满足实时性,就必须减少样本数量。Boosting的做法是对每一帧采集的样本仅使用一次便丢弃,然后进入下一帧采用新的样本。
以上就是在线Boosting算法的简单理解,具体而言,Boosting这里选择的弱分类器其实是Haar特征。由于Haar特征其实是一组特征,于是就需要Boosting算法根据每种Haar特征的响应来从Haar特征池中选出一个子集用于构造强分类器。

2008

IVT

IVT渐进地学习一个低维的子空间表示来自适应目标物体的变化,它将以前检测到的目标乘以遗忘因子作为样本在线更新特征空间的基而无需大量的标注样本。

2010

MOSSE

MOSSE(Minimum Output Sum of Squared Error)使用相关滤波来做目标跟踪(不是第一个,但可以看作前期的一个代表),其速度能够达到600多帧每秒,但是效果一般,这主要是因为它只使用了简单的raw pixel特征。
相比之前的算法,MOSSE能够形成更加明确的峰值,减少了漂移;此外,MOSSE可以在线更新,同时还采用了PSR来检测遮挡或者跟丢的情况,从而决定是否需要停止更新。
值得一提的是,MOSSE在做相关操作之前,对每张图都进行了减去平均值的处理,这有利于淡化背景对相关操作的影响。另外假如发生光照变化的话,减去均值也有利于减小这种变化的影响。此外要注意,输出的特征应乘以汉宁窗(一种余弦窗),用于确定搜索区域(也就是不为0的区域),且有利于突出中心的特征。

TLD

TLD(Tracking Learning Detection)主要针对long-term tracking,在跟踪的同时全局检测。它由三部分组成:跟踪模块、检测模块、学习模块。
跟踪模块观察帧与帧之间的目标的动向。作者采用了光流来跟踪,此外还提出了一种判断跟踪失效的算法,由于光流跟踪时选取的若干特征点,当其中某一个特征点的位移与所有特征点位移的中值之差过大时,也就是某个特征点离跟踪模块认为的目标中心位置很远时,就认为跟踪失效。作者还通过相似度和错误匹配度来对特征点进行筛选。
检测模块把每张图看成独立的,然后对单张图片进行目标检测定位。作者使用了方差检测器、随机森林和最近邻分类器来对目标做检测。
学习模块对根据跟踪模块的结果对检测模块的错误进行评估,当置信度较低时,重新组织正负样本对随机深林的后验概率和最近邻分类器的在线模板进行更新,从而避免以后出现类似错误。
TLD与传统跟踪算法的显著区别在于将传统的跟踪算法和传统的检测算法相结合来解决被跟踪目标在被跟踪过程中发生的形变、部分遮挡等问题。同时,通过一种改进的在线学习机制不断更新跟踪模块的“显著特征点”和检测模块的目标模型及相关参数,从而使得跟踪能够自适应,效果较之前更加稳定、可靠。

2011

FoT

FoT(Flock of Trackers)首先在目标上抓取多个interesting point并分别放入多个cell中,之后的跟踪就是检测并补偿每个cell中interesting point的偏动量,使其回到中间。如果interesting point超出cell,则让它重新恢复到cell的中点。

此外,FoT还提出了两种简单有效的failure预测方法:neighbourhood consistency predictor(Nh)和Markov predictor(Mp)。Nh的基本思想是认为正确的跟踪情况下每个local tracker给出的位移应当与它相邻的tracker相一致,而Mp主要是针对时域一致性,认为前几帧表现较好的local tracker在当前帧也会有较好的表现。FoT基于这些failure预测方法来控制模型的更新。

Struck

Struck的主要贡献是引入了结构化SVM。考虑到传统的跟踪算法将跟踪问题转化为一个分类问题,并通过在线学习技术更新目标模型。然而,为了达到更新的目的,通常需要将一些预估计的目标位置作为已知类别的训练样本,这些分类样本并不一定与实际目标一致,因此难以实现最佳的分类效果。
结合上述考虑,Struck利用了结构化SVM直接输出跟踪结果,避免了中间分类环节,这使得在当时效果有明显的提升。同时,为了保证实时性,Struck还引入了阈值机制,防止跟踪过程中支持向量的过增长。

L1 Tracker

L1 Tracker是第一个将稀疏编码引入目标跟踪问题中的算法。它把跟踪看做一个稀疏近似问题,主要是用第一帧和最近几帧得到的图像(特征)作为字典,通过求解L1范数最小化问题,实现对目标的跟踪。

MIL

MIL采用了多示例学习的方法而不是传统的监督学习(即由原本单独标记的示例变成一组示例,当且仅当所有的示例都判定为负才认为是负,只要有一个示例判定为正则整组都判定为正),对于不精准的tracker和错误标注的训练样本有鲁棒性的提升。

2012

CSK

CSK也称为核相关滤波算法,作者针对MOSSE做出了一些改进,作者认为循环移位能模拟当前正样本的所有的转换版本(除边界以外),因此采用循环移位进行密集采样,并通过核函数将低维线性空间映射到高维空间,提高了相关滤波器的鲁棒性。这里循环移位后的样本匹配可以理解为如果某个候选区域与某一个移位样本的相关操作响应较高,那么就可以理解为物体的移动和该样本移位的方式一致,从而对下一帧目标位置进行定位。
随后的工作主要从特征选择、尺度估计、正则化等方面对该算法进行改进和提高。关于循环移位和线性、非线性的核函数计算,我在之前的文章中做了一些分析,感兴趣的话可以看看。

DF

DF发现之前在图像中寻找目标的梯度下降方法首先会模糊图像来平滑目标方程,这就会严重损害目标的位置信息。因此作者提出了对每一个像素点设置多个通道,在每个通道进行卷积,这种方法同样也能平滑目标方程但不会严重损害目标的位置信息。其实这就是之后的CNN能做到的,在当时应该也算是一种创新。

CT

CT(Compressive Tracking)是一种基于压缩感知的高效跟踪算法。和一般的判别式模型架构一样,CT首先利用符合压缩感知RIP条件的随机感知矩阵对图像特征进行降维,使得到的低维信号可以完全保持高维信号的特性并可以完全重建,然后在降维后的特征上,在感知空间下采用朴素贝叶斯分类器进行分类。另外,CT在每一帧通过在线学习更新分类器,在线学习的样本来自通过相同的稀疏感知矩阵提取的前景目标和背景的特征。

ORIA

ORIA假设前一帧是完美的,于是把跟踪问题视作将下一帧图像与上一帧进行对齐,也就是一串连续的凸优化问题。

2013

下面是VOT2013的排名结果,其中Experiment 1是在所有序列上使用ground truth初始化的实验结果,Experiment 2使用含噪声(10%的尺寸扰动)的ground truth,Experiment 3使用灰度图像。

PLT

PLT通过一个固定大小的、基于二值特征向量的线性分类器对每一张图像做分类,得分最高即为目标。作者利用一个稀疏的在线结构化SVM来选出一个小的判别特征集合。在训练SVM时,考虑到在bounding box内的像素不一定都属于物体,作者使用了一种基于概率的掩模来分配权重,然后计算初始的结构化SVM,去除分值最小的特征。由于特征向量的二值性,该线性分类器可以作为查找表用于快速检测。

EDF

EDF是DF的加强版,在DF的基础上探索了每一个通道之间的联系。

LGT

LGT针对模型何时更新与更新哪些部分的问题,考虑到目标模型的整体更新会损失部分有用信息和固定的分块不利于应对目标的变化,借鉴了之前将目标有结构地分块且动态删减的思想,提出了一种由patch组成的集合构成的、用于精确定位的local layer和颜色、移位、形状三个特性组成的、用于指导增加patch的global layer。

对于新输入的一帧图像,LGT的处理流程如下:

  1. 首先使用卡尔曼滤波器结合近似匀速模型来确定目标的位置。接下来几步是对位置进行微调。
  2. 对于每一个patch,作者用一个统一的仿射变换和一个独立的微小扰动在5维空间(位置2维+尺度2维+旋转1维)定义其对于初始patch的变换。这里的$\widehat{x}$指的是初始patch。作者把$A_{t}^{G}$和$\Delta _{t}$中的参数看作正态分布的。先使用交叉熵方法反复迭代寻找最优解,当协方差矩阵的行列式小于0.1(各分量相关性很强)时停止迭代,随后把仿射变换矩阵的参数(也就是学到的正态分布的均值和方差)固定并用于所有的patch。接下来对每一个patch,用同样的交叉熵方法迭代,得到每一个patch的微小扰动的均值和方差。
  3. 结合visual consistency和drift from majority两种估计,更新每一个patch在当前帧的权重,并与前一帧的权重加权求和来确定最终的权重。这里的权重决定了每一个patch在最终所有patch混合决策时的重要性。
  4. 利用上面的patch重新计算之前卡尔曼滤波器的结果,确定目标的位置。
  5. 接着进行local layer中patch的删减与增补。对于权重小于阈值的patch作删去处理;对距离很近的两个patch进行合并,合并后产生的patch的所有参数设为合并前两个patch参数的平均。使用剩下的patch更新global layer,然后用更新后的global layer决定是否以及如何增加新的patch。为了防止突然过度增加patch,作者对增加的样本数施加上限限制,并利用加权的方式平滑调整样本容量。
  6. 进入下一帧。

LGT++

LGT++是LGT的改进版。在LGT的基础上,LGT++增加了memory、failure detection和用粒子滤波代替卡尔曼滤波的recovery机制。此外对尺度变化和背景干扰也做出了改进。

DLT

DLT是最早的基于深度学习的算法(当时AlexNet刚刚被提出),它采用了堆叠去噪自编码器网络,把跟踪视为一个分类问题,直接利用80 Million Tiny Images数据集上的预训练模型提取深度特征,这种强行task转换的训练方法存在缺陷,但在当时是个进步。

STC

STC(Spatio-Temporal Context)通过贝叶斯框架目标时间、空间的上下文信息来建模,利用得到的关系结合生物视觉系统中的注意力特性来生成confidence map来预测目标位置。由于上下文信息建模和之后的预测都采用了快速傅里叶变换,因此算法的速度很快。

文章主要举了两个例子来说明空间信息的重要性。当目标物体被部分或者完全遮挡时,周围的信息能帮助定位被遮挡的目标(假设摄像头不移动),也就是说可以利用空间的距离信息;此外,如果目标内部的两个部分比较相似(比如人的一对眼睛),就比较容易发生偏移,而如果这时恰好这两个部分的距离信息相似(距离目标中心长度相同),那就需要引入相对位置也就是方向信息来判断。
此外,考虑到生物视觉系统中的注意力特性,作者增加了一项权重函数来构成先验,该函数根据距离目标位置的远近来定义。
作者还对confidence map的参数进行了讨论,认为置信度在空间上的分布不能太平滑(增加位置模糊不确定性),也不能太尖锐(导致过拟合)。
作者认为目标的形态与近几帧有较强的关联,由此设计了时域信息模型。文中提到的时域滤波器可被证明是低通的,也就是可以滤去一定的噪声。此外,STC还设计了尺度更新方法,最终下一帧的尺度是前n帧估计尺度的均值。

LT-FLO

LT-FLO主要针对的是缺少纹理特征的目标,使用边界点来代替在目标上采集点来做跟踪。此外作者还提出了一种基于边界梯度稳定性的failure检测机制。

GSDT

GSDT提出了一种采集正负样本进行图嵌入的判别式模型。作者使用了基于图结构的分类器而不是生成一个子空间。此外GSDT还设计了一种新的图结构来区分类内的不同和样本的内在结构。

SCTT

SCTT使用了treelets降维方法,由于仅选取较高置信度的样本,相对于PCA只需要更少的样本且对噪声有更好的鲁棒性。

CCMS

CCMS(Color Correspondences Mean-Shift)用之前提到的Mean Shift方法对目标候选与目标模型、目标候选与背景模型在每种颜色(也就是直方图中每个bin)中计算相似性,反复迭代,直到收敛或者达到最大迭代次数为止,如此来进行运动估计。

Matrioska

Matrioska基于特征点提取的方法(ORB、FREAK、BRISK、SURF等),考虑到目标物体的外观变化和有利于增强模型表现力的负样本提取,使用了增枝和剪枝的方式来对目标模型进行更新。

AIF

AIF(adaptive integrated feature)提出了一种评估特征稳定性的方法,并根据不同特征的稳定性动态地分配权重。

HT

HT借鉴霍夫森林,也就是霍夫变换结合随机森林,相比一般的随机森林增加了位移信息。HT将目标分割成多个图像块,这些图像块含有它们各自偏离目标中心的向量$d$,对每个图像块提取特征描述子,这样就构成了正样本,即$y=1$;在图像的其他区域也提取同样尺寸的图像块,也提取特征描述子构成负样本,即$y=0$,注意负样本的偏移向量$d=0$。由此训练生成树,再由树构成森林。
在跟踪时,将每个图像块输入训练好的森林里,最终会落到森林的每棵树的一个叶节点上,这就得到了该图像块相对目标中心的偏移向量$d$以及概率$p$,随后将每棵树上的结果加权平均,得到了该图像块的结果。最后将所有的目标分割出的图像块的结果组合起来,得到目标预测结果。
HT的一个好处就是可以调整bounding box的长宽比,此外作者认为这对非刚性或者铰接的目标也有很大好处。

STMT

STMT把目标跟踪分成镜头运动估计和目标运动估计两个阶段,先估计摄像头的运动并进行对齐,然后再定位下一帧的目标位置。

ASAM

ASAM(Adaptive Sparse Appearance Model)用一个样本集来表示目标的各种变化,并且基于判别式和生成式的稀疏表示,使用了第一帧、后续帧两阶段的在线跟踪算法。

2014

下面是VOT2014的排名结果,这里的A表示accuracy,R表示robustness。

DSST

DSST主要考虑了尺度缩放的问题。它将目标跟踪看成位置变化和尺度变化两个独立问题,提出了一个高、宽、尺度数三维的滤波器,使用先计算平移位置再聚集尺度的“两步”法,即训练了两个滤波器,首先训练位置平移相关滤波器以检测目标中心平移,然后训练尺度相关滤波器来检测目标的尺度变化。

CN

CN(Color Naming)考虑到在遇到光照变化、形变、部分遮挡、背景干扰等问题时,颜色特征相比灰度特征能提供更丰富的信息以取得更好的效果,引入了颜色特征来扩展CSK,它将目标RGB(红绿蓝)三维空间的颜色特征映射为黑、蓝、棕、灰、绿、橙、粉、紫、红、白和黄11维空间的颜色特征的多通道颜色特征,后又降维至2维以保证实时性。Color Naming较RGB三原色特征更符合人类的感觉,对目标的表征能力更强,而且具有一定的光学不变性。

SAMF

SAMF也考虑了尺度问题,思路比较简单,采用k个尺度去采样,由于核相关操作的点乘需要固定尺度的输入,因此对采集到的样本作双线性插值成为固定尺度,然后再做相关操作。在特征方面,SAMF发现HOG和Color Naming有互补作用,考虑到和相关操作仅包含点乘和向量范数的计算使得多通道很容易被引入,因此使用了HOG和Color Naming多通道特征。

KCF

KCF跟CSK是同一个团队提出的,它跟CSK的区别是就是作者对循环性质进行了完整的理论推导,引入HOG特征并提供了一种把多通道特征融合进相关滤波框架的方法,对CSK作了进一步的完善,是一个具有里程碑意义的工作。算法的详解和一些数学理论可以看看我之前的文章。

DCF

DCF与KCF出自同一篇paper,不同的是KCF使用的是高斯核,DCF使用的是线性核。

注意:这里的DCF(Dual Correlation Filter)和之后一些文章中提到的DCF(Discriminative Correlation Filter)是两个不同的概念,请注意,别搞错了。

FCT

FCT(Fast Compressive Tracking)和CT一样,也是使用了压缩感知,主打速度。相比之前,FCT提出了一种由粗到精的搜索策略,而不是穷尽搜索。首先在一个较大的搜索半径内选择一个较大的搜索步长,得到一个粗糙的位置,然后以该位置为中心,在一个较小的搜索范围内,以一个较小的搜索步长进行搜索,最后得到跟踪目标的位置,这样就能在不降低最终精度的前提下加速寻找过程。由于可证明CT特征具有尺度不变特性,FCT在采集候选区域时增加了尺度因子,即在同一位置采集三个尺度的候选区域,从而得到当前帧的尺度。此外,作者采用了每5帧更新一次尺度的策略。

CMT

CMT用成对的特征点之间角度的变化来判断目标的旋转情况,此外还用特征点投票的方式来确定目标的位置。为了避免尺度变化引起的投票不准,也就是从特征点出发指向目标位置的投票向量越过了目标中心或者没达到目标中心,作者用欧氏空间和原图像空间之间各对特征点之间距离的比值来进行修正。基于投票出的目标位置的聚类,作者还给出了一种一致性的判别方法,将聚集最多数特征点的投票位置视为一致性聚类,并将投票至其他位置的特征点视为错误从而移除。

2015

下面是VOT2015的排名结果。

SO-DLT

SO-DLT针对DLT的缺陷进行改进,使得CNN更加适用于目标跟踪。由于目标跟踪的目的是将物体从背景中分离出来而不是全图识别,DLT的训练方法和标签化输出就不是很合适了。
但是,由于当时跟踪方向标注数据的匮乏,作者还是不得不使用ImageNet图像检测数据集来进行预训练,事实证明这是有效的,因为目标检测和目标跟踪两个不同的task中存在一样的共性信息。不同于标签化的单个数值输出,SO-DLT输出的是一个50x50的像素级的概率图。
由于上述训练方法训练的是CNN从非物体中提取出物体的能力,因此在实际跟踪接收到第一帧时,还需根据目标对网络进行微调,否则会跟踪出视频或者图像序列中所有的无论是目标与否的物体。
类似DSST,SO-DLT采用的是先预测目标中心位置,然后再从小到大确定尺度的策略,如若扩展到预设的最大尺度来检测的概率图依旧达不到要求,则认为已经跟丢目标。
此外,为了提升鲁棒性,作者采用了两个CNN网络共同决策而以不同方式更新的策略。两个网络分别针对的是short-term和long-term。针对short-term的网络在负样本的概率图响应和超过一定阈值时进行更新,为的是防止负样本与目标响应近似而导致漂移;针对long-term的网络在当前帧预测结果的置信度达到一定水平以上时才进行更新,因为此时可认为框出的目标较为可信。
每次更新时需要采集正负样本,SO-DLT对正负样本的提取方法比较简单,在目标位置及周围形成一个类似九宫格的区域,在中间格用四种尺度提取正样本,对周围的8格采集负样本。

MDNet

MDNet设计了一个轻量级的小型网络学习卷积特征表示目标。作者提出了一个多域的网络框架,将一个视频序列视为一个域,其中共享的部分用来学习目标的特征表达,独立的全连接层则用于学习针对特定视频序列的softmax分类。
在离线训练时,针对每个视频序列构建一个新的检测分支进行训练,而特征提取网络是共享的。这样特征提取网络可以学习到通用性更强的与域无关的特征。
在跟踪时,保留并固定特征提取网络,针对跟踪序列构建一个新的分支检测部分,用第1帧样本在线训练检测部分之后再利用跟踪结果生成正负样本来微调检测分支。
此外,MDNet在训练时还采用了难例挖掘技术,随着训练的进行增大样本的分类难度。

SRDCF

SRDCF主要考虑到若仅使用单纯的相关滤波,可能会存在边界效应,也就是相关滤波采用循环移位采样导致当目标移位到边缘时会被分割开,此时得到的样本中就没有完整的目标图像从而失去效果。
于是,作者采用了大的采样区域,用两倍区域进行循环移位,这就保证了无论如何移位,获得的样本中都能有一个完整的目标存在。
然而,由于上述移位方式会导致背景信息被夹在横向以及纵向的两个样本之间而出现在图片的中间区域,这会导致模型判别力的退化。因此作者在移位之前先在滤波器系数上加入权重约束(类似于惩罚项):越靠近边缘权重越大,越靠近中心权重越小。这就使得滤波器系数主要集中在中心区域,从而让背景信息的影响没有那么明显。尽管作者采用了埃尔米特矩阵的共轭对称性来提升计算量,但是由于SRDCF破坏了原本闭式解的结构使得优化问题只能通过迭代求解,因此速度比较缓慢。

DeepSRDCF

DeepSRDCF在SRDCF的基础上,将handcrafted的特征换为CNN的特征,关注点也在解决边界效应。作者使用DCF作为网络的最后一层,也就是对之前卷积网络输出的每一个通道的CNN特征都训练一个滤波器用于分类。作者还对不同的特征进行了实验,说明了CNN特征在解决跟踪的问题采取底层的特征效果会比较好(DeepSRDCF仅用了PCA降维处理的第一层),说明了跟踪问题并不需要太高的语义信息。

HCF

HCF的主要贡献是把相关滤波中的HOG特征换成了深度特征,它使用的是VGG的3、4、5三个层来提取特征,针对每层CNN训练一个过滤器,并且按照从深到浅的顺序使用相关滤波,然后利用深层得到的结果来引导浅层从而减少搜索空间。

FCNT

FCNT较早地利用CNN网络底层和顶层不同的表达效果来做跟踪。不同于以往的工作把CNN看成一个黑盒而不关注不同层的表现,FCNT关注了不同层的功能,即发现:顶层的CNN layer编码了更多的关于语义特征的信息并且可以作为类别检测器;而底层的CNN layer关注了更多局部特征,这有助于将目标从目标中分离出来。这个发现在之后的许多工作中也得到了应用和体现。如下图所示,这里的a图表示的是ground truth,b图表示的是使用VGG的conv4-3,也就是第10层产生的热力图,c图是通过conv5-3也就是第13层产生的热力图。

可以看到,较低维的CNN layer(conv4-3)能够更精准地表示目标的细粒度信息,而较高维的CNN layer(conv5-3)热力图显示较模糊,但对同类别的人也做出了响应。这就是说,顶层缺少类内特征区分,对类间识别比较好,更适合作语义分割;底层则反之,能够更好地表达目标的类内特征和位置信息。
基于不同层(顶层和底层)之间提取特征的不同,作者提出了一种新的tracking方法,利用两种特征相互补充辅助,来处理剧烈的外观变化(顶层特征发挥的作用)和区分目标本身(底层特征发挥的作用)。由于feature map本身是有内在结构的,有很多的feature map对目标的表达其实并没有起到作用,因此作者设计了一种方法来自动选择高维CNN(GNet)或者低维CNN(SNet)上的feature map,同时忽略另一个feature map和噪声。在线跟踪时,两个网络一起跟踪,采用不同的更新策略,并在不同的情况下选择不同的网络输出来进行预测。
顺便提一下,为了简化学习任务,降低模型复杂度,作者采用了稀疏表示的方法。
关于FCNT的一些相关概念和具体按步骤的细节实现,可以参考一下我之前写的文章。

LCT

LCT主要针对的是long-term tracking的问题。作者配置了一个detector,用于跟丢之后快速重检测。LCT用了两个滤波器,一个是用于平移估算的$R_{c}$,使用padding并施加汉宁窗(一种余弦窗),结合了FHOG和一些其他的特征;另一个是用于尺度估计的$R_{t}$,不使用padding和汉宁窗,使用HOG特征,此外$R_{t}$还用于检测置信度,用来决定是否更新模型和是否重检测。

CCT

CCT借鉴KCF中kernel trick的特性和DSST中将定位和尺度估计两步分离的思想,对DSST只利用本来的特征空间表征目标的不足进行改进,在核特征空间对目标进行表征并用尺度因子扩展KCF的核相关滤波器,也就是为了保证计算效率和连贯性,利用尺度因子将每一帧目标尺度都统一为初始帧的目标尺度,然后使用核相关滤波器进行位置估计。然后,通过和DSST一样的方式再进行尺度估计。
为了应对漂移的问题,CCT使用了一个在线CUR滤波器。CUR矩阵分解可以近似的表示原矩阵A,其中C是A的列而R是A的行,两者通过一种固定的方式从A中随机采样形成,这既保证了A的内在结构,可以反映A的低秩属性,也可以看作一种映射,即将过去的目标表征矩阵投影到一个可被证明具有误差上界的子空间。此外,作者还引入了一个基于失败检测的自适应学习率调整方法。

CFLB

CFLB讨论了循环移位带来的边界效应的问题,提出了在目标外围扩大尺寸来进行循环移位的方法,使得有效样本的比例大大提高。具体来说,就是给原来的循环移位样本左乘一个列数远大于行数的掩模矩阵。此外,由于扩大尺寸后部分参数要在空间域而不是频域计算会导致效率降低,作者利用了增广拉格朗日方法(即在拉格朗日方法的基础上添加了二次惩罚项,从而使得转换后的问题能够更容易求解,不至于因为条件数变大不好求)来解决这个问题。

KCFDP

KCFDP借鉴了目标检测中detection proposal的思想(主要用于减少计算和提高质量),来解决之前DCF系列算法中的尺度和长宽比变化的问题。对每一帧,KCFDP首先用KCF对上一帧输入(准确的说是之前每一帧的加权累积)作操作,得到当前帧的位置和响应分数$v$;随后利用EdgeBoxes(一种detection proposal方法)在KCF预测的位置周围搜寻proposal,选取其中的前200个,并排除其中与之前KCF得出的预测目标IoU大于0.9(认为结果一样,无需考虑)或者小于0.6(认为误判,不是目标);最后,在剩余的proposal中,选择得分最高的proposal,与之前的响应分数$v$作比较:若小于$v$,则把KCF的结果作为预测结果且不更新尺度和长宽比(KCF算法本身具有该功能);若大于$v$,则把该proposal作为预测结果,并利用该proposal的尺度和长宽比来更新目标的参数。

HCFT

HCFT构造了一种阶梯式的深层至浅层由粗到细的定位方法,结合深层网络的语义信息和浅层网络的高分辨率位置信息,其网络结构如下(和FPN很像)。

为了保持分辨率相同,作者对池化后的深层输出再进行双线性插值以复原原来的分辨率。假设$l$层最大响应处的坐标为$(\widehat{m},\widehat{n})$,HCFT通过以下式子来确定$l-1$层目标的位置。

第二行的约束是为了浅层细粒度的位置需保持在深层粗粒度的位置附近,这样便完成了由粗到细的定位方法。
此外,在训练过程中需要采集正负样本,由于正负样本边界难以区分的模糊性和二值(也就是0,1标注)的正负样本的绝对性导致一点微小的正负样本区别就会导致drift。为此,作者将训练样本的标注回归到高斯方程的平滑标签。

MUSTer

MUSTer模拟了人脑的记忆过程,类似于LSTM那样分成short-term和long-term两种memory,使用了相关滤波(short-term)和特征点检测(short-term+long-term),最后根据两种记忆形式的提供的输出来决策和进行滤波器的更新。
人脑的记忆分为感官、短时记忆、长时记忆三个阶段,MUSTer的设计基本采用了这样的三个step,如下图所示。

MUSTer的结构比较“纵横交错”,下面我对一些比较重要的部分做一个概述。
短时记忆和长时记忆都由特征点的集合构成。特征点数据集包括目标和背景两种样本,其中背景主要是用于遮挡的判断,即当位于bounding box中的背景特征点与目标特征点的比例超过一定值时,认为此时发生了遮挡。
对于特征点的匹配,作者采用了最近邻方法,在欧几里得空间根据余弦相似度(也就是两个向量余弦夹角的大小,角度越小,余弦值越大,相似度越高)来计算匹配置信度。为了判别离群值(outlier),还需计算第二近邻的相似度,如果第一近邻的相似度比上第二近邻的比值小于某个阈值,就说明该处特征点比较集中应该不是离群值。
长时记忆模块通过RANSAC估计的一个版本——MLESAC(引入似然度)来决定目标的状态,从而与相关滤波的输出结合。
短时记忆在每一帧都进行更新,若根据前面所说的方法判定为遮挡,则清空短时记忆;若此时并没有判定为遮挡,则用RANSAC估计输出的内围值(inlier)来替换之前的短时记忆。此外,为了避免多余的特征点出现,作者用网格划分目标template并根据相对位置分配ID,若出现重复的ID,则认为两者中之前的特征点是多余的。
长时记忆只在判断跟踪成功和无遮挡时进行更新。作者认为匹配失败的特征点能够表示目标发生变化的重要信息,因此长时记忆更新是针对匹配失败的点进行的,将匹配失败且位于bounding box外面的点移入背景数据集,而将匹配失败且位于bounding box内部的点移入目标数据集。模拟人脑,长时记忆采用的是一种对数形式下降的遗忘曲线。

RPAC

RPAC将相关滤波器应用于分块跟踪,并且借鉴了粒子滤波中的贝叶斯估计的思想,在提升鲁棒性的同时保证了速度。
作者对每一区块(part)都使用一个独立的相关滤波器,使用了PSR(体现置信度)和时域顺滑程度(用于判断遮挡等情况)两者结合来分配每一区块的权重,同时仅当这个权重大于阈值时才更新对应的滤波器以达到自适应更新的效果。这里每一个区块的输出都是一个confidence map,最后需要根据权重和相对位置组成一个大的confidence map用于接下来的预测。
为了防止部分区块漂移的问题出现,作者采用了贝叶斯估计框架,即选择使得状态(一组仿射运动参数)先验值最大的候选区域作为结果,考虑到多个confidence map之间重叠的部分直接求和会出现叠加的较大值而影响概率估计,作者根据每一个区块的最大响应值和尺寸来施加余弦窗,从而抑制多张图中较小值的叠加改变某些区块位置上的响应分布。
此外,对于偏离较远的区块,RPAC采用自动丢弃并利用其他区块重新生成的方法。得益于分块的方式,使得tracker对尺度变化也有适应性。

RPT

RPT把目标物体看作一系列具有相似运动轨迹的patch的集合。作者基于粒子滤波的框架,用patch的两个属性来定义每个patch的可靠性:可追踪性和目标附着性。可追踪性直接用KCF输出的响应图取PSR来定义;目标附着性根据每个patch在近k帧的运动轨迹来定义,具体来说,就是认为正样本与其他正样本的运动轨迹是一致的且远离负样本,同时负样本与正样本的运动轨迹有很大的差别。其公式定义如下,其中$y_{t}$是正样本或者负样本的$\pm 1$标签(正负样本用bounding box来分割)。

由于粒子滤波是用函数(这里是可靠性函数)对后验概率分布做近似,由于无法达到理想状态(即完全一致),因此为了避免噪声的累积,粒子滤波类算法需要重采样来不断补入正确的信息。RPT采用的是不断补入新样本的方法而不是全部重采样替换的方法,作者在两种情形下采样新的样本:当正样本或者负样本其中一者的比例过高时,采集新样本来平衡;当跟踪置信度(这里定义为PSR)较低时,采集新样本,这种情况往往出现在遇到缺乏纹理的目标物体时。此外,作者对离目标过远的patch进行舍弃,具体就是划定一个比bounding box更大的矩形框,对超出这个矩形框的patch做舍弃。
可以说,作者把patch分成三类。第一类是positive patch,也就是在目标上的;第二类是贴着目标周围一圈的negative patch,这对下一帧区分物体和背景很有帮助;第三类就是离目标很远的negative patch,这些patch的作用就比较弱,因此舍弃。

2016

下面是VOT2016的排名结果。

DLSSVM

DLSSVM延续之前的Struck,利用结构化SVM,在优化的阶段做了一些改进进行提速。其实结构化SVM分类器非常强大,但是因为它求解优化的过程比较复杂以及使用稠密采样(粒子滤波或者滑窗采样)比较耗时,使得结构化SVM的速度成为一个瓶颈,因此不如一些使用相关滤波的SOTA的算法。

C-COT

C-COT(连续空间域卷积操作)发现单一分辨率的输出结果存在扰动,因此作者想到利用CNN中的浅层表观信息和深层语义信息相结合。然而之前的DCF系列算法仅能使用单一分辨率的特征图,这就无法使用预训练CNN中不同分辨率的不同层,这是限制其效果的重要因素。因此,作者提出一种连续周期的插值运算符以利用不同空间分辨率的响应,在频域进行插值得到连续空间分辨率的响应图,最后通过迭代求解最佳位置和尺度(用0.96,0.98,1.00,1.02,1.04五种缩放倍率去搜索)。
和名称一样,C-COT最重要的贡献是它把图像$N_{d}$个像素点的离散分布变成了周期为$T$的连续空间响应图。由于本文的理论功底很深,我之前并没有看懂,后来经学长的指点才有所感悟。事实上,这里的插值运算符做的并不是插值的事,而是用一种近似的方式将离散信号重构为连续信号。虽然会导致计算量剧增,但这是一个很重大的突破。
文中还提出了一种使得各个分辨率通道的特征自然融合至相同分辨率的方法,这里相同分辨率可理解为最后的各个响应图在空间上拥有相同的样本点数。作者首先对各个不同分辨率的通道进行插值,然后使用对应的滤波器在连续的空间域内卷积,最后将响应求和得到最终的置信度方程。
C-COT使用的是类似SRDCF的框架,也引入了空间正则项,当远离目标中心是施加较大的惩罚,这使得能够通过控制滤波器的大小来学习任意大小的图像区域。C-COT在每一帧也会采集一个训练样本,根据过去帧数的远近来设置每个采集样本的重要性权重(每次都做归一化),并且设置了最大的样本容量,当超出容量时删去重要性权值最小的样本。不同于SRDCF使用Gauss-Seidel迭代法,C-COT使用Conjugate Gradient方法来提高效率。
得益于浅层特征的高分辨率,C-COT能够达到sub-pixel的精度,也就是仅次于像素级别的精确度。位置细化的过程就是上面所说的用共轭梯度法迭代的过程,在C-COT的代码中有一个迭代次数设置,被设置为1,即就使用一步迭代优化后的位置。换句话说,在当前长时跟踪算法本身误差之下,更精细的位置意义不大。

SRDCFdecon

SRDCFdecon针对在线跟踪时采集的样本中有一部分质量不佳的问题,不同于之前把采样和样本的选择作为一个独立的模块,作者提出了一种将样本权重统一到模型参数中的损失函数。
不同于之前“加入训练集or舍弃”这样二选一的样本选取方式,SRDCFdecon使得样本的重要性权重连续,同时在跟踪的过程中能够完成权重的重新分配和先验的动态变化。这里的先验其实可以看作对权重的一种约束,使权重在近几帧逐渐增大。作者用一种比较巧妙的方式来控制先验的影响力,具体来说,对权重的正则项为$\frac{1}{\mu }\sum_{k=1}^{t}\frac{\alpha _{k}^{2}}{\rho _{k}}$,当$\mu$趋向于无穷时,损失函数求解得到仅有当前帧的权重$\alpha$趋向于1,相当于丢弃了之前的样本,仅接收并保存当前帧的新样本;当$\mu$趋向于零时,$\alpha$将趋向于先验$\rho$。

Staple

Staple提出了一种互补的方式。考虑到HOG特征对形变和运动模糊比较敏感,但是对颜色变化能够达到很好的跟踪效果,color特征对颜色比较敏感,但是对形变和运动模糊能够有很好的跟踪效果,因此作者认为若能将两者互补就能够解决跟踪过程当中遇到的一些主要问题。于是,Staple使用HOG-KCF与color-KCF结合算法对目标进行跟踪,速度很快,效果也很好。

SINT

SINT运用匹配学习的思想,最早地把孪生网络(Siamese Network)应用于目标跟踪。它通过孪生网络直接学习目标模板和候选目标的匹配函数,并且在online tracking的过程中只用初始帧的目标作为模板来实现跟踪。

TCNN

TCNN使用一个树形的结构来处理CNN特征。作者利用可靠性来分配预测目标的权重,采用的更新策略是每10帧删除最前的节点,同时创建一个新的CNN节点,选择能够使新节点的可靠性最高的节点作为其父节点。这样一直保持一个active set,里面是10个最新更新的CNN模型,用这个active set来做跟踪。TCNN效果较之前有一定提升,但是速度比较慢,而且比较消耗存储空间。

SKCF

SKCF把之前KCF中用于确定搜索区域的余弦窗换成了高斯窗,这么做有两点主要的好处。
首先,当搜索区域固定时,余弦窗的带宽就是固定的了,而高斯窗则可以通过调整方差来改变中间响应比较高的区域的宽度。可以这么理解,我们从二维的余弦函数和高斯函数来看,假设搜索区域的宽度是$\pi $,那么距离边缘$\frac{\pi }{6}$的位置一定是中间最大值下降一半的位置,而高斯函数的形状则还受方差控制。这一特性使得高斯窗在搜索区域确定时能够更好地适应目标尺寸,此外论文中还提到了这种方法能方便减轻计算量。
此外,高斯分布的傅里叶变换依旧是高斯分布。这种特性可以抑制频谱泄露的问题。简单来说,频谱泄露就是信号从时域转化到频域后,除了原本应该出现的谱线,其旁边还会漏出一些小的频谱,从而造成干扰。因此,抑制频谱泄露能够保持前景与背景在频域内的区分度。
SKCF还改进了之前一些基于特征点的算法的不足。由于之前的一些算法在最终决策时用矩形框来划定有效的特征点,并分配相同的权重做出决策。作者认为这样相当于间接的认为目标是矩形的,而大多数目标的几何结构往往不是矩形。于是SKCF对各个特征点采用从中心到周围逐渐减少权重分配的方式,能更好地适应目标的几何结构。
SKCF还是用了英特尔CCS(复数共轭对称)文件格式,无论是用在SKCF还是KCF上,计算速度相比原来的KCF都提升了将近一倍,或许是共轭对称减少了一半计算量。

MRF

MRF提出了一种基于马尔科夫随机场的模型,挖掘各个patch和目标之间的联系(弹性能量)并判断每个patch的遮挡情况。作者发现当响应值较低时并不能认为该patch被遮挡了,也有可能是目标外观变化等情况。此外作者还发现当发生遮挡时,会出现较大的响应值分散分布的现象。于是当patch中响应值高于$\eta s_{max}^{k}$的像素个数占比高于阈值时(这里的$\eta$是一个小于1的因子),就认为该patch被遮挡。
对于尺度变化,作者发现当尺度变小时patch之间的重叠会变多,因此通过计算初始帧的patch距离和当前帧的patch距离之比来确定尺度缩放的比例。
不同于之前的马尔科夫随机场模型,作者在文中还给出了一种高效的置信度传播方法。

GOTURN

GOTURN提出了一种类似孪生网络的框架,将裁剪过的前一帧和当前帧分别通过在ImageNet上预训练的backbone提取特征,然后用三层全连接层进行特征比较并进行回归。其中前一帧的图像在裁剪时将其置于裁剪后图像的中心,并在周围做一定的扩充以吸收更多上下文信息。当前帧裁剪区域也就是搜索区域由上一帧的位置来确定。
GOTURN最突出的贡献就是把基于深度神经网络的速度第一次达到了100FPS。作者通过在视频和静态图像上进行离线训练,在跟踪时不更新来达到这种效果。由于训练集的缺乏,作者冻结之前在ImageNet上预训练的CNN权重,在离线训练时仅更新FC层。考虑到实际跟踪时的特点,作者还设计一种平滑的运动模型。作者发现目标相邻两帧中心点的坐标关于尺度的增量呈均值为0的拉普拉斯分布(类似高斯分布,只不过中间是尖的),也就是说一般物体逐帧的运动是较小的。因此,作者对训练数据做增广处理,使得随机裁剪得到的样本能服从拉普拉斯分布。由于训练方式和网络设计,使得GOTURN仅对目标敏感而不是对类敏感(比如行人检测能检测各种行人而不能检测车)。

SiamFC

SiamFC使用孪生网络来解决数据稀少和实时性要求对深度学习在目标跟踪中的限制。作者以第一帧的BBox的中心为中心裁剪出一块图像,并将其缩放至127x127作为template,并保持不变。在后续帧中,search image也用类似的方法得到。分别将template和search image通过5层不带padding且不在线更新的AlexNet,然后用互相关层做相关操作得到输出的score map。响应最大值和中心的偏差表示位移。此外作者用一个小批量的不同尺度的图像去前向传播来实现多尺度判断。
由于对于search image来说,表示通过CNN的卷积方程是全卷积的,因此可以使用比template大的search image,也就是可以在它上面的各个子窗口进行计算。
基于卷积的平移等变性,我们可以通过score map得到目标的位置,即当目标平移了$n$时,相应的就会在score map上平移$\frac{n}{stride}$。之所以仅使用5层而不是更深的网络,是因为SiamFC没有使用padding来使得网络能够更深。之所以不使用padding,是因为一旦加入了padding,会使得图像边缘像素的响应值在平移的同时会发生改变,这就不利于最后的定位了。具体来讲,就是当目标处于画面中央时,padding进来的是context;而当目标处于画面边缘时,padding进来的就是0了,这种信息的不同会影响定位和目标的判断。

2017

下面是VOT2017在隐藏数据集上的排名结果。

ECO

ECO(高效卷积算子)主要是为了解决C-COT速度慢的问题。从参数降维、样本分组和更新策略三个角度对其改进,在不影响算法精确度的同时,将算法速度提高了一个数量级。
为了减少模型参数,考虑到在C-COT中许多滤波器的能量(可理解为贡献)小得几乎可以忽略,因此ECO使用了一个较小的滤波器子集,且原来的滤波器都可以用这个子集中的滤波器线性组合表示(即乘上一个行数为高维数、列数为低维数的矩阵)。子集的选取方法就是简单的选取能量高于某一阈值的滤波器,其效果类似于PCA。作者提出了一个因子化的卷积算子来学习这个子集,用PCA初始化,然后仅在第一帧有监督地优化这个降维矩阵,在之后的帧中直接使用,相比C-COT模型参数量大大降低,同时也减轻了计算和存储的负担。

为了减少样本数量,作者提出了一个紧凑的样本空间生成模型,采用高斯混合模型(GMM,可理解为当有多个聚类时用多个不同的高斯模型来表示更好)来合并相似样本。当GMM的聚类(component)数量超过阈值时,如果有权重低于阈值的component,则丢弃之;否则,就合并最近的两个component。如此就可以建立更具代表性和多样性的样本集,既保持样本之间的差异性,也减少了存储的样本数量。

此外,作者还提出了一种稀疏的更新策略,即每隔N帧(实验发现5帧左右最好)才更新一次参数。由于样本集是每帧更新的,这种稀疏更新策略并不会错过间隔期的样本变化信息。此外,这种方法的另一个好处就是把原本的用逐帧单独样本进行更新,变成了用连续几帧所采集的样本所构成的batch来进行批处理的更新,这样就减小了在某帧遮挡、突变时过拟合的可能性。由此,稀疏更新策略不但提高了算法速度,而且提高了算法的稳定性。

CREST

CREST提出了将DCF构建成一层卷积神经网络,并且引入了残差学习来应对目标外观变化带来的模型退化。
考虑到之前的DCF系列没有发挥端到端训练的优势和空间卷积与相关滤波中循环输入点乘的相似性,作者用一层卷积神经网络来代替DCF的作用,这不仅使得模型能够通过反向传播训练,同时因为没有使用循环移位,避免了边界效应。
由于上述一层网络难以达成在多种情况下网络输出和ground truth的一致(模型复杂度较低易受干扰),而若使用多层网络很可能会导致模型退化(我理解为过拟合导致的),作者引入空间残差和时间残差。设我们希望最佳的输出为$H(x)$,而上述单层网络的输出是$F_{B}(x)$,为了补足某些时候(尤其是复杂情况下)单层网络的输出与希望最佳的输出之间的差距,引入残差项$F_{R}(x)=H(x)-F_{B}(x)$。在训练时,$F_{B}(x)$和$F_{R}(x)$中的参数一起训练,使得遇到特殊情况(遮挡、运动模糊等)时,$F_{R}(x)$能够补足纠正$F_{B}(x)$不稳定的响应结果。
空间残差和单层网络都是利用当前帧作为输入,考虑到空间残差有时候也会失效,作者又引入了把初始帧作为输入的时间残差,最终表达式如下:

注意:论文中第一项为$F_{R}(X_{t})$,可能有误,为此我作了修改。

另外,CREST采用当前帧最大响应尺度和上一帧尺度加权求和的方法来决定当前帧的最终预测尺度,从而使尺度能够平滑地变化。

LMCF

LMCF借鉴了KCF的循环特征图、Struck的结构化SVM,使用相关滤波在频域加速计算,从而解决了之前结构化SVM系列算法(Struck、DLSSVM)的速度问题。
在前向追踪时,LMCF考虑到画面中相似物体的干扰,提出了一种多峰值的目标跟踪算法(Multimodal Target Tracking),即对高于某一阈值的响应峰值做二次检测,把response map和一个用于筛选的二值矩阵作点乘,相当于把不是峰值的位置滤为0。对于通过筛选的峰值,以每个峰值为中心提取patch并用之前的方法再计算一遍峰值,取此时最大的峰值作为结果。
在模型更新时,LMCF提出了一种高置信度的更新策略(High-confidence Update),由于LMCF主要关注的是实时性,所以希望在算法简单的情况下能够减少失误。在传统的方法中,一般是当最大响应的峰值高于某一个阈值时(认为没跟丢目标),就对模型进行更新;否则若没有响应值超过峰值,就不对模型进行更新。而该工作的实验发现,当目标被遮挡时,响应图会震荡得非常厉害(存在多个较大的峰值),但同时最大响应的峰值仍旧会很高,这就会指导模型进行错误的更新并导致最后跟丢目标。于是作者提出了一个APCE值,定义如下。

只有当最大响应的峰值比较明确,即远超response map中的其他的响应时,APCE值才会比较大。因此LMCF仅当最大峰值和APCE超过阈值时才允许对模型进行参数和尺度模型的更新。

DeepLMCF

同LMCF,不同之处是使用了CNN特征。

MCPF

MCPF结合多任务相关滤波器(MCF)和粒子滤波器,这里的多任务相关滤波器指的是利用了多种特征滤波器之间的相关性。作者对K种特征,定义了参数$z_{k}$去选择具有判别力的训练样本。作者发现,各个特征中的$z_{k}$往往会选择具有相同循环移位的样本,因此不同的$z_{k}$应该具有相似性和一致性。为此,作者在损失函数中增加了矩阵Z的混和范数。
考虑到粒子滤波通过密集采样来覆盖状态空间中的所有状态,这会大大增加计算量,而且并不能保证很好地包括目标物体在一些情况下的状态。因此作者利用MCF对每个采样的粒子进行引导,使其更接近目标的状态分布。这样就可以在提升效果的同时每次采集较少的粒子,从而提高计算效率。另一方面,粒子滤波的密集采样能够在目标尺度发生变化时覆盖状态空间,这就解决了单一相关滤波器的尺度问题。
算法的流程分为四步:
首先,使用转移模型生成粒子并且重采样。
然后,使用MCF对粒子进行微调,使其转移到比较合适的位置。
接着,利用响应更新MCF的参数。
最后,通过求样本均值问题来决定目标的状态,也就是位置等参数。
可见,这里相关滤波仅起到指导的作用,最后的决策由粒子滤波器做出。
顺便一提,MCPF使用了Accelerated Proximal Gradient来解决这里不可微分的凸优化问题(含有范数)。

CFNet

CFNet结合相关滤波的高效性和CNN的判别力,考虑到端到端训练的优势,从理论对相关滤波在CNN中的应用进行了推导,并将相关滤波改写成可微分的神经网络层,将特征提取网络整合到一起以实现端到端优化,从而训练与相关滤波器相匹配的卷积特征。
CFNet采用孪生网络的架构,训练样本(这里指用来匹配的模板)和测试样本(搜索的图像区域)通过一个相同的网络,然后只将训练样本做相关滤波操作,形成一个对变化有鲁棒性的模板。为了抑制边界效应,作者施加了余弦窗并在之后又对训练样本进行了裁剪。
在对比实验中作者发现仅使用一层卷积层时CFNet相比Baseline+CF效果提升最显著,对此作者的解释是可以把相关滤波层理解为测试时的先验知识编码,当获得足够的数据和容量时(增加CNN层数时),这个先验知识就会变得冗余甚至是过度限制。

2018

下面是VOT2018 short-term的排名结果。

STRCF

STRCF(时空正则相关滤波器)主要针对SRDCF的速度做出改进,同时在精度上也有很好的提高。作者发现SRDCF速度很慢的两个原因是:每次对多张图片进行训练打破了循环矩阵的结构,从而无法发挥循环矩阵的计算优势;巨大的线性方程组和Gauss-Seidel迭代法没有闭式解,效率较低。对此,STRCF提出了引入时间正则和ADMM算法。
受online Passive-Aggressive learning的启发,STRCF在SRDCF空间正则的基础上引入了时间正则。我们可以对比两者的回归求解公式具体来看一下。
SRDCF:

STRCF:

这里的$w$表示空间正则化矩阵,越靠近边缘值越大;$f$表示相关滤波器,$f_{t-1}$表示的是$t-1$帧时的滤波器。
忽略每项之前的常数系数,我们可以看到两式的第二项是一样的,也就是STRCF保留了SRDCF的空间正则来抑制边界效应;在第一项中,STRCF没有对过去的每一帧进行求和来训练,这就减小了计算量;同时STRCF加入了第三项时间正则,使得新得到的滤波器与之前的滤波器之间的变化尽可能小,相当于保留了之前的信息。
这么做有两点好处:首先,STRCF可以看作SRDCF的一个合理近似,能很好地发挥后者同样的作用;此外,由于时间正则的引入,使得STRCF不易于在当前帧上过拟合,在遇到遮挡或者超出画面等问题时,STRCF能很好地保持与之前滤波器的相似度从而降低了跟踪器完全跟丢到另一个物体上去的可能,这一定程度上提高了STRCF的精度。

此外,ADMM算法的引入使得最优化求解问题有了闭式解,这比Gauss-Seidel迭代法用稀疏矩阵求解要快得多。得益于SRDCF的凸性,ADMM也能收敛到全局最优点。

UPDT

UPDT区别对待深度特征和浅层特征,主要考虑的是缺少数据和深层卷积在增加语义的同时降低分辨率这两个问题。作者分析了数据增强(flip,rotation,shift,blur,dropout)和鲁棒性训练(也就是tracker应对各种复杂场景和恢复的能力,可以通过扩大正样本的采样范围来训练)对deep feature和shallow feature分别的影响,发现deep feature能通过数据增强来提升效果,同时deep feature主打的是鲁棒性而不是精度;相反,shallow feature经数据增强后反而降低了效果,但同时它能够很好地保证精度。因此,作者得出了深度模型和浅层模型应该独立训练,最后再融合的方案。
作者在文中还定义了Prediction Quality Measure,考虑了精度和鲁棒性,精度用响应分数的锋利程度(sharpness)来体现,鲁棒性则用响应值的幅度来表示,幅度越高表明tracker越确信跟踪的目标,也就是鲁棒性越高。关于具体公式的推导和分析,以及Prediction Quality Measure在预测过程中的具体使用可以看一看原文。

ACT

ACT使用了强化学习,构建了由Actor和Critic组成的学习框架。离线训练时,通过Critic指导Actor进行强化学习;在线跟踪时,使用Actor来定位,Critic进行验证使得tracker更加鲁棒。不同于之前的搜索方案(随机采样或者通过一系列分离的action来定位),ACT希望的是搜索一步到位。这步最优的action也就是离线强化学习所关注的行动,而强化学习的状态由输入到网络中bounding box中框出的图片定义,奖励值根据IoU来定义。
在训练的过程中,由于action space比较大,因此要获得一个正奖励比较困难(随机采取action的话IoU恰好高于阈值的可能性较小)。因此作者利用了第一帧的信息来初始化Actor以适应新的环境。同样的,由于巨大的action space,原本DDPG方法中的噪声引入就不适合跟踪任务了,因此在训练前期,Actor采取的行动以某种概率被一种专家决策所替代。随着训练的进行,Actor越来越强大,这时就逐渐减弱专家决策的指导作用。
在跟踪的初始帧,作者首先在第一帧提供的ground truth周围采集多个样本。然后使用Actor对这些样本作action,根据得分对Actor进行一次微调;对Critic,根据打分和ground truth也进行一次初始化训练。
在之后的跟踪过程中,若Critic的给分大于0,则采用Actor的输出一步到位地预测下一帧的目标;否则,再使用Critic在上一帧周围采集的样本中选出最优作为目标,完成重定向。此外,可以认为Actor在离线训练时已经比较稳定了,因此在跟踪过程中只对Critic进行更新,且仅在Critic给分小于0(认为Critic没能很好地适应目标的变化)时,取前十帧的样本来更新Critic。

DRT

DRT引入了可靠性的概念,考虑到空间正则、掩模等抑制边界效应的方法都不能抑制bounding box内部的背景信息,同时这些方法会导致滤波器的权重倾向于集中在某些较小的区域(主要是中央的关键区域,我理解为边缘区域被抑制掉了,因此学习时自然不会去分配权重),作者认为这是不利于目标跟踪的(容易个别不可靠的区域被误导)。为此,作者提出了DRT,它主要是将滤波器分成了一个base filter和一个reliability term的element-wise product:

这里的base filter用于区分目标和背景;reliability term用于决定每片区域的reliability,由目标区域每一个patch的reliability值加权求和决定:

这里的$p$是对每一个patch的掩模,用于确定每个patch做相关操作的区域;$\beta$有上下界的限定,目的就是为了降低feature map中响应不平衡的影响,防止由于响应的集中而导致仅有一小块区域被关注。

需要最小化的目标方程包括三项:分类误差、局部一致性约束和滤波器参数$h$的二范数。分类误差就是与ground truth之间的损失函数,计算时需考虑可靠性;局部一致性约束用于减小循环样本中的每一个片段的响应差距,该项不受$\beta$即可靠性的影响,也就是说base filter在训练时依旧要保持对每个局部区域同样的关注度,使得base filter能独立于可靠性进行训练,这就避免了前面提到的滤波器在训练时边缘区域被抑制所造成权重集中的后果;滤波器参数$h$的二范数用于保证模型的简单程度,防止模型退化(可以理解为过拟合)。
由于只有当base filter的参数$h$和reliability的权重$\beta$有一项已知时,目标方程的最小化问题才是凸优化问题,因此作者采用了$h$、$\beta$交替训练的方法。
作者还使用权重逐帧退化的方式设计了一种简单的利用多帧信息的目标方程。借鉴ECO,DRT也采用了间隔几帧更新一次的稀疏更新方法和基于高斯混合模型的样本分组策略。类似DSST,DRT采用了先确定位置再计算多个尺度的响应的“两步”尺度估计方法。

MCCT

MCCT使用了多特征集成学习,在跟踪时对每一帧分别选用最合适的特征来做出决策。为了应对不同的场景,MCCT选择了low,middle,high三个层级的特征,并通过排列组合得出7种expert。尽管有些特征的鲁棒性明显差于三类特征的组合,但是它们提供的多样性对集成学习是至关重要的。

为了评估每个expert在每一帧的好坏以决定具体选用哪一个,作者提出了Expert Pair-Evaluation和Expert Self-Evaluation。
Expert Pair-Evaluation分为两项:在第一项中,作者认为一个expert的好坏可以通过它与其他expert的整体一致性来体现,于是首先计算了每个expert相对于其他6个expert在当前帧预测结果的一致性(通过重叠率来衡量)之和;此外,作者认为一个好的expert还必须是temporal stable的,因此他又计算了每个expert相对于其他6个expert在前几帧内预测趋势的一致性,这就可以防止因为在当前帧碰巧预测一致而导致之前一项的分值很好的情况,也保证了expert的可信度。最后两项结合得到Expert Pair-Evaluation。
在Expert Self-Evaluation中,作者认为路径的顺滑程度一定程度上能够体现每个expert的可靠程度。
最后将Expert Pair-Evaluation和Expert Self-Evaluation加权求和选出每帧最好的expert做出决策。
MCCT提出了一种peak-to-sidelobe ratio和鲁棒性的置信度分数来进行模型更新:

其中,$P_{mean}^{t}$是每个expert响应图peak-to-sidelobe ratio的平均,$R_{mean}^{t}$亦然。当$R_{mean}^{t}$比较低时,认为采集到了不可靠的样本(比如遮挡问题等)。为此,作者的模型更新策略是,当置信度分数$S^{t}$大于之前置信度均值时,采用正常学习率,否则,根据置信度算出一个较小的学习率以在一定程度上维持模型。
为了提升速度,每个expert之间共享了样本和RoI,最后MCCT的速度为7.8FPS,MCCT-H(没采用深度特征)的速度为44.8FPS。(作为参考,ECO的速度为15FPS)

LSART

LSART分析了深度特征中的空间信息,提出了两种互补的回归方式来使得跟踪更加鲁棒。
作者首先对比了CNN-based和KRR-based(核岭回归)两类tracker,认为它们各有利弊且是互补的。由于KRR的循环采样,目标的结构特征会被打破,对形变和遮挡问题效果不好,而CNN则能够很好地提取位置信息;相反,CNN庞大的参数量使得它容易过拟合,而KRR-based tracker就不会出现这样的问题。因此,若将两者结合(将热力图加权求和),就可以让KRR关注全局而让CNN关注较小、较精确的目标,进而达到更好的效果。
对于KRR,作者引入cross-patch similarity,将参数看作训练样本的加权求和,将响应项拆分成三个模块,这就方便把原本的迭代求解的方式分成三步在神经网络中来求解了。
对于CNN,考虑到形变和遮挡等问题会使得目标的一部分比其他区域更加重要,不同于以往在feature map上做文章,作者对卷积层的滤波器施加掩模,使得各个滤波器关注于不同的区域,在跟踪的过程中,这些掩模不做变化。此外,作者还提出了距离变换池化层用于评判输入feature map的可靠性。另外,作者设计了一种two-stream的训练网络,将空间正则的卷积层和距离变换池化层分开训练以防止过拟合,能够比较好的处理旋转问题。

SiamRPN

SiamRPN利用了Faster RCNN中的RPN,解决了之前深度学习跟踪算法没有domain specific(可理解为类间不区分)以及还需额外的尺度检测与在线微调的问题。RPN回归网络的引入,一方面提高了精度,另一方面回归过程代替多尺度检测,使得速度有所提升。
在在线跟踪时,SiamRPN将跟踪看作one-shot检测的问题,也就是用第一帧目标样本的信息来预测RPN网络中的参数,从而实现domain specific且不需要在线更新。作者把template分支和detection分支卷积视作类别信息在RPN网络上的embedding。

DaSiamRPN

DaSiamRPN在之前的孪生网络系列的基础上增加了distractor-aware,这里的distractor指的是在判别式方法中,不同于无语义信息易判别的背景,而存在一定的语义并对前景分割存在干扰的背景。这其中的一大原因是之前的训练集仅从同一个视频序列的不同帧中采样,造成了non-semantic的背景样本具有较大的比重而semantic的背景样本较少,这就弱化了模型准确判别前景的能力。此外,之前的孪生网络系列还存在不能在线更新和不进行全局搜索这两个问题。
首先,作者提出了三类样本选取方法来弥补传统采样的不足。考虑到视频数据集中类别缺乏和标注的难度,作者引入了ImageNet和COCO图像检测两个数据集,并把样本分成三类对tracker进行训练。

对于正样本对,其作用是提升tracker的泛化能力和回归精度;对于来自同一类别的样本对,其作用是让tracker更注重细粒度的表达方式,提升判别能力;对于来自不同类别的样本对,其作用是让tracker在遮挡、超出视野等情况下拥有更好的鲁棒性。
值得一提,作者发现motion pattern能很好地被浅层网络建模,因此在数据增强时还引入了运动模糊。
DaSiamRPN通过上述方法对数据做了增强,可是在跟踪特定目标时,还是很难将一般模型转化为特定视频域所用。考虑到上下文信息和时域信息可以提供特定目标的信息以增加tracker的判别能力,作者提出了一个distractor-aware module。具体来说,在上一帧中选择出的proposal中,通过非极大抑制处理,剩下的proposal中最大的就是目标,剩下的就是会产生误导的distractor;在当前帧,为了抑制这些distractor的干扰,可以减去这些distractor之前响应的加权和,减去之后还是最大的proposal就是我们要找的目标,其基本思想如下公式所示:

这里的$\alpha$是控制distractor影响大小的权重系数,作者又对上式进行调整,通过引入学习率使得该分类器在线可学习,这就无需利用反向传播更新网络参数,而通过微调一个分类器弥补了传统基于孪生网络的tracker不能在线更新的缺点。
此外,当认为目标跟丢时,DaSiamRPN会匀速扩大搜索范围,并且通过高效的bounding box回归来代替图像金字塔,这就通过一个简单的方法在应对长时跟踪目标消失问题时较之前基于孪生网络的tracker取得了一个进步。

Meta-Tracker

Meta-Tracker将元学习运用在了目标模型的初始化上。作者认为结合深度特征和在线学习的模型有两大困难,一是训练的样本不容易获得,二是大多数SOTA的tracker在训练阶段都需要花费大量的时间在初始化上面。
针对上面的难题,作者提出了一种在未来的帧上训练目标模型的思路,采用了基于预测梯度的策略学习方法获得普适性的初始化模型,使得跟踪模型自适应于后续帧特征的最佳梯度方向,从而在接收到第一帧时仅需一步迭代就能使参数快速收敛到合适的位置。这样做有三点好处,一是能使模型更加关注对后续的帧更有价值的特征,二是避免了在当前帧上过拟合,三是能够使初始化更快速。总而言之,就是能保证精度和鲁棒性。
考虑到上述方法在长序列或者目标在帧与帧之间变化不大时表现不佳(会偏离目标),这是因为Meta-Tracker一步到位的思想使得学习率会偏大。因此作者仅在模型初始化时采用学习到的学习率,在之后的跟踪过程中仍旧沿用原来版本的方式进行更新。这里原来的版本指的是CREST和MDNet,作者在这两个tracker的基础上改进出了MetaCREST和MetaSDNet。具体的改进和处理可以看一看我之前写过关于Meta-Tracker的文章。

DorT

DorT(Detect or Track)把跟踪看作一个连续决策的过程,它结合目标检测和目标跟踪两个领域内SOTA的结果,在孪生网络输出的结果上再添加一个小型的CNN网络作为scheduler来判断在下一帧是作检测还是作跟踪。

LADCF

LADCF针对DCF系列的边界效应和模型退化(后者主要是单帧独立学习和模型更新速率固定导致)的问题,提出了一种空间域特征选择和时间域约束结合的方法,并且使其能在低维流形中有效表示。

补充:流形学习的观点认为,我们所能观察到的数据实际上是由一个低维流形映射到高维空间上的。由于数据内部特征的限制,一些高维中的数据会产生维度上的冗余,实际上只需要比较低的维度就能唯一地表示。

掩模策略应用于目标跟踪时,仅将目标区域的参数激活。LADCF也运用了这个思想,对滤波器中的参数$\theta$作降维处理$\theta _{\phi }=diag(\phi )\theta$,这里$\phi$中的元素要么是0、要么是1,即不激活或者激活。不同于PCA和LLE,这种方法在降维的同时也保持了空间特性,不仅能加速求解,也能除去大部分干扰,使滤波器关注于目标部分从而可以使用更大的搜索域。
最后的目标函数如下:

可以看到,这里还包括与历史模型的正则项,减轻了滤波器退化。作者让$\lambda _{1}< < \lambda _{2}$,也就是让时间域上的一致性更加重要于特征的稀疏选取。

FlowTrack

不同于之前先把光流算好,FlowTrack是第一个把光流信息进行端到端训练的,这无疑提高了光流使用的精度。作者注意到之前的算法大都采用RGB特征也就是外观特征,且缺少对运动特征和帧与帧之间联系的利用,这就导致在部分遮挡和形变等情况下效果会变差。不同于把之前的几帧保留下来等时域方法,作者希望能把前几帧的特征直接补充到当前帧上融合成一个,而具备方向和速度信息的光流就成了一种比较合适的方法。为此,作者将光流引入孪生网络框架,使得仅使用外观特征的一些不足得到弥补。
作者的思想很巧妙,我以遮挡问题为例简述一下。作者的想法是当当前帧的目标有部分被遮挡时,我们可以根据光流的运动信息,把前几帧的特征映射过来对齐,通过插值补全当前帧的特征,简单来说可以理解为把当前帧缺的那块给补全。为了有效地选择补过来的特征,作者使用了空间注意力和时间注意力两种机制结合。空间注意力分别计算前几帧补过来的特征与当前帧的特征的相似度,根据相似度大小来分配特征在各个空间位置上的权重。但是由于最近的一帧总是与当前帧的特征最为相似,它的权重肯定是最大的,考虑到假如最近一帧由于遮挡等原因等导致特征质量下降而不适合分配最大的权重,因此还需要时间注意力来重新校准权重。具体来说,时间注意力作用在空间注意力的输出上,对于一般情况下的目标基本不会改变空间注意力的输出,而对遮挡等情况下的帧就会减小其在空间注意力中分配到的权重。

VITAL

VITAL针对正样本高度重叠无法捕获目标丰富的特征变化和正负样本不平衡的问题,采用对抗学习的思想,分别设计了生成网络和代价敏感损失来解决这两个问题。
这里的生成网络输出为一个作用于目标特征的掩模。通过对抗学习,该生成网络可以产生能保留目标特征中较为鲁棒的部分。其目的是对仅在个别帧判别力强的特征进行削弱,防止判别器过拟合于某个样本。个人理解,这里的判别力强的特征并不是指的能够很好地区分目标和背景的特征,而是指仅在某些帧出现比较独特,而在大多数情况下不存在于目标上的特征。
考虑到很容易被分类正确的负样本在训练过程中也会产生损失,然而我们并不希望网络关注这些损失,因为关注他们反而会使得网络性能变差。因此,为了避免很多负样本主导损失函数,作者采用高阶敏感损失,减小简单负样本的权重,这不但提升了精度,也加速了网络的收敛。

SA-Siam

SA-Siam考虑到语义信息和外观信息的互补关系,构建了两个并行的孪生网络。在外观网络(作者使用的是SiamFC)具有判别力的基础上,结合语义网络(作者使用了AlexNet的第4和第5层作为backbone)更泛化、更鲁棒的语义信息,使得tracker的效果更好。
外观网络的输入为目标和搜索区域,其框架跟SiamFC基本一致,这里就不细说了。
不同于外观网络,语义网络的输入为目标及其背景和搜索区域(两者一样大),作者直接使用了AlexNet提取高层的语义信息并不进行训练,因为假如训练了的话就跟外观网络接近了,而集成学习的思想就是被集成的各个模型之间的关联性应比较弱,这也是文中多次强调的。在提取高维语义信息之后,为了使得得到的语义特征更适合于跟踪任务,作者使用了1x1的卷积层作了一次特征的fusion。此外,考虑到对不同的追踪目标各个通道的重要性是不同的,作者还设计了一个通道注意力模块,具体而言,就是对每一个通道,先做最大池化变成3x3的网格,然后再通过一个多层感知机来决定每个通道的每个格子的重要性。由于通道注意力的选择也受目标周围背景的影响,因此最大池化的结果仅中间的格子是目标区域,周围的8个格子代表目标周围的背景信息。再次强调,在训练过程中,语义网络冻结语义特征提取的AlexNet,仅训练特征fusion和通道注意力模块。
根据上文的解释,同样的,为了保持语义网络和外观网络的弱关联性和互补性,两支网络独立训练,仅在测试时加权得到最后的相似度图作为预测依据。

2019

下面是VOT2019 short-term的排名结果。

GFS-DCF

GFS-DCF考虑到深度网络的高维通道存在许多冗余的信息,因此作者在时域和空间域之外,还考虑了通道维度的影响,在目标函数中使用了三个正则项。

注:建议结合上图来看接下来的分析。

对于空间域,作者将每个通道(也就是每个feature map)的对应点相连接,用范数约束,可以理解为提取那些在绝大多数特征图中都是最重要的特征的位置。

从通道角度,作者又把每一个通道作为一项来做约束,可以理解为提取那些特征比较重要的通道。

对于时域,作者使用了low-rank约束,这里的rank指的是矩阵的秩而不是排名,low-rank主要用于图像对齐(alignment),在文中的目标是最小化$rank(W_{t})-rank(W_{t-1})$,这里的$W_{t}$表示从1到t每个滤波器向量化后形成的矩阵。下面是作者最后修改后的正则项:

注:上述三项在实际目标函数中还要加上权重。

作者发现,空间正则对使用handcrafted特征的模型效果显著,而对使用CNN的模型(文中是ResNet)效果提升不大;相反,通道正则对使用handcrafted特征的模型效果不明显,而对使用CNN的模型效果显著。作者在文中解释认为由于深层CNN特征表示的语义信息丰富而缺少细粒度的信息,因此相比保留更多空间结构handcrafted特征,对深层CNN特征使用空间正则比较难以判别哪些位置的特征反应了目标位置的信息。此外,由于在训练过程中一些通道的权重下降到很小,也就是说模型本身就不怎么关注这些通道,因此使用通道正则在这里取得了比较明显的效果。

D3S

D3S考虑到BBox对目标的粗糙表示会影响性能以及视频分割任务中对背景干扰和长时视频不鲁棒的问题,提出了一种视频跟踪、视频分割互补的框架。

如上图所示,作者构造了用于分割的GIM和用于定位的GEM两个模块。
GIM使用初始帧的目标像素点构造目标的特征向量,使用初始帧目标周围的像素点构造背景的特征向量。在之后的图像中,每个像素点的目标相似度,定义为该点和每个目标的特征向量做相似度计算之后,最大的K个目标相似度的均值;同理,每个像素点的背景相似度,定义为该点和每个背景的特征向量做相似度计算之后,最大的K个背景相似度的均值。最后将目标相似度图和背景相似度图作softmax得到分割结果。该模块没考虑位置信息,用分割使得在应对剧烈形变的目标时能够取得较好的效果。
然而,GIM的分割结果对相似的物体或者背景的干扰并不鲁棒,因此我们希望能有鲁棒的位置信息来提升判别力。GEM就是直接使用一个DCF来找到最大响应值的位置,也就是目标的中心位置,然后以该中心为圆心,向周围每个像素点根据半径由大到小分配置信度。最后仅把GIM中得到的且由GEM分配的置信度高于阈值的目标像素点作为分割结果。
由于通过backbone的encode导致此时的输出分辨率较低,因此还需要作上采样。具体来讲,就是每次把输入扩大到两倍分辨率,通过两次卷积,然后加上backbone中对应分辨率的层。
由于预训练的backbone特征缺少精细的划分,因此作者在初始帧先进行降维的训练。具体来讲,就是首先将预训练网络通过1x1的卷积来降维,再通过一层3x3的卷积,从而达到调整网络参数使得分支划分得到最佳的目的。
由于输出是二值掩模,因此作者还对使用BBox的目标跟踪问题作了变换处理。首先,在初始帧,除了进行降维和DCF的训练,作者还先把BBox内部的像素点视为目标点,把BBox外4倍区域的像素点视为背景点,用D3S在第一帧上迭代(文中说只迭代1次就可以了)以产生比BBox更细致的分割,将最后分割出的目标点和其周围的背景点用于构造特征向量,在之后的跟踪过程中保持不变并用于GIM模块。此外,在输出时,作者先用椭圆去近似掩模,然后根据长轴和短轴来确定BBox。由于椭圆的确定遗漏的背景信息(仅考虑怎么把mask包进来)从而导致BBox偏大,因此作者提出了一种考虑背景信息的方法,对长轴进行微调来确定最终的BBox。

GlobalTrack

GlobalTrack关注的是long-term tracking的问题。我们知道,在目标跟踪问题中,为了更好的利用前一帧甚至前几帧的信息,往往会对模型做很多假设,包括目标的运动、位置变化、尺度变化(假设平滑变化等等),而这些假设并不能很好地处理所有的情况(比如位置或尺度突变、目标消失、短时跟踪失败等),由此产生了模型的累计误差。而在长时跟踪问题中,这样的累计误差往往会使得后期的目标跟踪结果差很多。
基于上述考虑,GlobalTrack根据长时跟踪的特点,把跟踪看作在每一帧作全局检测的问题,设计了一种没有运动模型、没有在线学习、没有位置估计、没有尺度平滑的无累积误差的baseline,其基本框架如下图所示。

受Faster RCNN启发,GlobalTrack也是基于two-stage的框架。其下面的一条和Faster RCNN基本一致,由于Faster RCNN是目标检测类算法,其目的是在图像中框出所有物体并分类。而目标跟踪仅需要目标,因此作者用添加了上面一条query-specific的引导。为了简单起见,作者把query的RoI看成kxk的方型特征,在第一个feature modulation中,作者把RoI特征卷积成1x1的卷积核,然后再和通过backbone的搜索图像特征做卷积相关操作,得到query-specific的候选框;在第二个feature modulation,作者把RoI特征与每个候选框作哈达玛积(也就是简单地将两个尺寸相同的矩阵的对应位置作乘积),由此来改进标签置信度和BBox的预测。
在训练时,作者取多对图像对,其中每一对图像都共同含有M个相同的实例,作者用每对图像来相互预测每对各自的M个实例,使总的损失达到最小以进行训练。
在测试时,作者简单地把第一帧作为query,之后的每一帧都视为独立的全局检测,直接取得分最高的BBox作为结果,如此就不存在依赖相邻帧带来的累计误差了,因此作者认为视频长度越长,GlobalTrack的表现就越突出。

SPSTracker

SPSTracker针对目标周围噪声引起的响应(sub-peak)导致模型漂移,以及由多尺度样本加权得到的特征图响应最大值和目标真正的几何中心不一致的问题,提出了BRT和PRP两个模块。作者使用的是ATOM的框架,两个模块在框架中的使用方式如下图所示。

这里的BRT其实就是简单的将远离目标中心的点置为0,其目的是减小响应图的方差,使得响应图尽可能地呈现单峰响应的形状,此外也起到了抑制边界效应的效果。PRP其实就是作者新设计的一个池化层,该池化层把每一像素点的值设为该像素点所在列所有像素点中的最大值和所在行所有像素点的最大值之和,从而使响应值能更加靠近目标的几何中心,从而能够更好地使用多尺度样本。作者对搜索区域使用BRT,然后将通过分类器的置信度图通过一个PRP构造的残差模块,从而达到抑制sub-peak的目的。

SiamRCNN

SiamRCNN发现重检测很容易受到干扰物的影响从而产生模型漂移,从难例挖掘和运动轨迹动态规划两个角度入手,设计了一个利用第一帧和前一帧为模板的孪生网络检测结构,在短时跟踪评价上能与之前SOTA的算法持平,在长时跟踪评价上有非常显著的进步。注意,这里的追踪轨迹不仅追踪目标,同时也追踪所有的distractor。

SiamRCNN可以说综合了许多前人的成果。为了收集更多难例,作者在不同的视频上根据嵌入网络上的距离来获得更多负样本。接下来,我结合上面的网络结构图来简述一下我对该算法的理解。
首先,作者使用RPN网络获得了类别无关的proposal,然后和第一帧结合,通过一个与Faster RCNN前半段相似的Re-Detection Head。接着,通过3个级联的RCNN获得回归后的bounding box(这里借鉴了Cascade RCNN,其认为高质量的proposal能够产生更好的结果,3级RCNN的IoU阈值设定逐级升高)。随后,作者将此时获得的bounding box与前一帧所有的检测结果两两组合(也就是即检测目标也检测干扰物),再次输入到之前相同的重检测结构中。最后对得到的所有结果进行跟踪轨迹动态规划。
在轨迹动态规划模块,作者计算轨迹与检测结果的相似度,用不具有不确定性的检测结果去延续之前的若干条轨迹。这里的不确定性定义为:对这条跟踪轨迹,有其他的检测结果和本结果具有相当的相似度;对检测结果,有其他的跟踪轨迹和本轨迹具有相当的相似度。对于不确定的检测结果,作者让它们开启新的轨迹。对于没有确定检测结果的轨迹则暂时中断,其中也包括含有初始帧的轨迹,当它中断时可认为目标消失了。


研究趋势

以下是我对近几年来目标跟踪领域各种算法主流的研究趋势和发展方向的一个浅析,个人思考,多多指教。

注:其实近几年还出现了一些其他的关注方向,由于不是主流、目前关注较少、本人学识不够等原因,在此不做列举。

信息提取

深度特征

早期的目标跟踪算法主要在handcrafted特征方面进行探索和改进,以2012年AlexNet问世为节点,深度特征开始被引入目标跟踪领域。
我们知道,在现实场景中,物体是在三维的运动场中移动的。而视频或图像序列都是二维的信息,这其实是一些难题的根本原因之一。一个比较极端的例子就是理发店门前经常会出现的旋转柱,如果单纯地从二维角度来看,柱子是向上运动的,可在实际的运动场中柱子是横向运动的,观测和实际的运动方向是完全垂直的。

因此,为了能够更好地跟踪目标,我们需要提取尽可能好的特征,此外最好能从视频或图像序列中学到更多丰富的信息(尤其是含语义的)。值得注意的一点是,在港科大王乃岩博士2015年所做的ablation experiments中(详见参考文献[2]),发现特征提取是影响tracker效果最重要的因素。
考虑到精度是保证目标跟踪鲁棒性的重要因素,不同于一些其他的计算机视觉任务,目标跟踪领域的深度算法比较强调结合与充分利用浅层网络的高分辨率信息。

时域和空间域结合

可以说,在目标跟踪的相关算法中,空间正则指的就是抑制边界效应。由于CNN能够在学习的过程中能够产生对样本中各个区域有区分的关注度,因此可以不考虑边界效应。对边界效应的处理主要是在相关滤波类等需要循环移位的算法中出现。
事实上,目标跟踪这一个任务本身就在利用时域信息,因为预测下一帧肯定需要上一帧的信息,然而仅仅利用上一帧的信息往往是不够的,充分的利用时域信息在正则或者辅助记忆方面都可以取得一定的效果。

学习方式

结合语义分割的多任务学习

由于bounding box粗糙的对目标的标注表示使得有不少冗余的背景信息进入template,此外bounding box对平面内旋转等场景不鲁棒,这些都会导致模型的退化。早期也有许多算法考虑到了分割,但主要是对目标进行分块(part-based)而不是语义分割。由于跟踪的目标是有具体形状的且是一起运动的,同时判别式方法正是要分离除目标以外的物体,因此近年来有不少算法结合语义分割,通过多任务学习来进一步提高tracker的效果。具体来说,就是引入掩模(mask)来识别预测物体,最后在转化成bounding box作为结果。

元学习

实际上,目标跟踪这一个任务本身的特性就决定了它与元学习的思想有共通之处。元学习主要针对的是两个问题:在少样本学习的情况下对样本的利用效率比较低;当进行一个新的任务时对之前学到的经验的可移植性差,我觉得这里的新任务可以指从分类到跟踪这样类别之间的转换,也可以指在不同的视频序列上训练和测试这样“域”之间的转换。
当深度特征兴起之后,目标跟踪中的许多算法都选择迁移目标分类任务中的一些预训练模型来提取特征,这种迁移学习其实就包含了元学习的思想。MDNet将每个视频看做一个域,在测试时新建一个域但同时保留了之前训练时在其他域上学到的经验,既能够更快更好地在新的视频序列上学习也避免了过拟合。孪生网络实际上也是元学习领域一种比较常用的结构,它学习了如何去学习输入之间的相似度。

其他关注点

样本采集

样本采集主要包括样本的数量、样本的有效性、正负样本和难易样本的平衡性。

防止过拟合

每帧获取的少量信息和目标的意外变化导致信息的丢失,使得过拟合问题成为目标跟踪任务中一个比较重要的关注点,下面是一些比较常见的方法:

  • 冻结一些层的参数(设置学习率为0),仅更新一部分层的权重。
  • 采用coarse-to-fine的网络框架,用语义信息来避免过拟合。
  • 通过在目标方程中添加正则项来限制模型的稀疏度,也即参数量。
  • 采用two-stream的模型结构,短时更新和长时更新相结合。
  • 采用稀疏更新的方式(隔几帧更新一次),相当于将利用单帧信息的更新变成了批处理的形式。
  • 每次更新采用最近几帧的信息而不是只用目前帧的信息,其原理类似上一条。
  • 利用初始帧或者质量比较好的几帧存储的样本来进行时域正则。
  • 对不同的情况采用不同的更新或者初始化的策略。
  • 使用dropout来防止过拟合。
  • 使用基于patch的跟踪算法。
  • 使用掩模去除不可靠的信息。

碰到底线咯 后面没有啦

本文标题:computer vision笔记:目标跟踪的小总结

文章作者:高深远

发布时间:2020年02月15日 - 21:42

最后更新:2020年04月18日 - 13:00

原始链接:https://gsy00517.github.io/computer-vision20200215214240/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
database笔记:范式的理解 | 高深远的博客

database笔记:范式的理解

今天终于完成了计算机三级数据库的考试,这也是本学期的第一门考试。听说计算机三级中要属计算机网络最简单,然而出于学到更多有用的知识的目的,我报了数据库。然而事实证明也没学到多少,毕竟这个计算机等级考试是给非计算机专业的人设置的,现在只求能过。不过两三天书看下来,还是有些收获,现在考完了有时间就在这里记一下,方便自己和别人今后有需要看。

References

电子文献:
https://blog.csdn.net/he626shidizai/article/details/90707037
https://blog.csdn.net/u013011841/article/details/39023859


范式

注意:本文中的范式指的是数据库范式。

在设计数据库时,为了设计一个良好的逻辑关系,必须要使关系受一定条件的约束,这种约束逐渐成为一种规范,就是我们所说的范式。
目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF)。要求最低的是1NF,往后依次变得严格。其中最后的5NF又称完美范式。
数据库一般只需满足3NF,下面我就介绍一下前三种范式。

第一范式

数据库考试官方教程并没有对每个范式的定义进行讲解,另外因为文字定义比较晦涩难懂,我这里通过多方参考,用图片的形式来展示各个约束条件。
首先,1NF是所有关系型数据库最基本的要求,它的定义为:符合1NF的关系中的每个属性都不可再分。下图就是一个违反1NF的例子:

修改如下:

上面的情况就符合1NF了。
我们还可以把第一范式分成两点来理解:

  1. 每个字段都只能存放单一值

    还是上反例: 上图中,第一行的课程有两个值,这就不符合第一范式了。因此要修改成这样:
  2. 每笔记录都要能用一个唯一的主键识别

    这里出现了重复组,同样也不满足1NF,因为缺乏唯一的标识码。因此修改如下:

第二范式

第二范式是建立在第一范式的基础上的,它的改进在于:消除了非主属性对于码的部分函数依赖。
第二范式消除了非主属性对于码的部分函数依赖,也就是说,第二范式中所有非主属性完全依赖于主键,即不能依赖于主键的一部分属性。
为了解释明白,还是通过实例的说明:

上表中,学号和课程号组合在一起是主键,但是姓名只由学号决定,这就违反了第二范式。同样的,课程名只由课程号决定,这也违反了第二范式。此外,只需要知道学号和课程号就能知道成绩。
为了满足第二范式,我们就需要对上表做如下拆分:

第三范式

同样的,第三范式建立在第二范式的基础上。不同之处在于,在第二范式的基础之上,第三范式中非主属性都不传递依赖于主键。
这是什么意思?还是看图说话:

上表中,主键是学号,且已满足第二范式。然而,学校的地址也可以根据学校名称来确定,第三范式就是在这里再做一个分解:


碰到底线咯 后面没有啦

本文标题:database笔记:范式的理解

文章作者:高深远

发布时间:2019年09月21日 - 19:58

最后更新:2020年01月19日 - 08:23

原始链接:https://gsy00517.github.io/database20190921195840/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
database笔记:范式的理解 | 高深远的博客

database笔记:范式的理解

今天终于完成了计算机三级数据库的考试,这也是本学期的第一门考试。听说计算机三级中要属计算机网络最简单,然而出于学到更多有用的知识的目的,我报了数据库。然而事实证明也没学到多少,毕竟这个计算机等级考试是给非计算机专业的人设置的,现在只求能过。不过两三天书看下来,还是有些收获,现在考完了有时间就在这里记一下,方便自己和别人今后有需要看。

References

电子文献:
https://blog.csdn.net/he626shidizai/article/details/90707037
https://blog.csdn.net/u013011841/article/details/39023859


范式

注意:本文中的范式指的是数据库范式。

在设计数据库时,为了设计一个良好的逻辑关系,必须要使关系受一定条件的约束,这种约束逐渐成为一种规范,就是我们所说的范式。
目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF)。要求最低的是1NF,往后依次变得严格。其中最后的5NF又称完美范式。
数据库一般只需满足3NF,下面我就介绍一下前三种范式。

第一范式

数据库考试官方教程并没有对每个范式的定义进行讲解,另外因为文字定义比较晦涩难懂,我这里通过多方参考,用图片的形式来展示各个约束条件。
首先,1NF是所有关系型数据库最基本的要求,它的定义为:符合1NF的关系中的每个属性都不可再分。下图就是一个违反1NF的例子:

修改如下:

上面的情况就符合1NF了。
我们还可以把第一范式分成两点来理解:

  1. 每个字段都只能存放单一值

    还是上反例: 上图中,第一行的课程有两个值,这就不符合第一范式了。因此要修改成这样:
  2. 每笔记录都要能用一个唯一的主键识别

    这里出现了重复组,同样也不满足1NF,因为缺乏唯一的标识码。因此修改如下:

第二范式

第二范式是建立在第一范式的基础上的,它的改进在于:消除了非主属性对于码的部分函数依赖。
第二范式消除了非主属性对于码的部分函数依赖,也就是说,第二范式中所有非主属性完全依赖于主键,即不能依赖于主键的一部分属性。
为了解释明白,还是通过实例的说明:

上表中,学号和课程号组合在一起是主键,但是姓名只由学号决定,这就违反了第二范式。同样的,课程名只由课程号决定,这也违反了第二范式。此外,只需要知道学号和课程号就能知道成绩。
为了满足第二范式,我们就需要对上表做如下拆分:

第三范式

同样的,第三范式建立在第二范式的基础上。不同之处在于,在第二范式的基础之上,第三范式中非主属性都不传递依赖于主键。
这是什么意思?还是看图说话:

上表中,主键是学号,且已满足第二范式。然而,学校的地址也可以根据学校名称来确定,第三范式就是在这里再做一个分解:


碰到底线咯 后面没有啦

本文标题:database笔记:范式的理解

文章作者:高深远

发布时间:2019年09月21日 - 19:58

最后更新:2020年01月19日 - 08:23

原始链接:https://gsy00517.github.io/database20190921195840/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:一篇非常经典的论文——NatureDeepReview | 高深远的博客

deep learning笔记:一篇非常经典的论文——NatureDeepReview

这是一篇非常经典的有关深度学习的论文,最近在看一个网课的时候又被提到了,因此特地找了pdf文档放在这里和大家分享。之后还分享了一篇deep-learning笔记:近年来深度学习的重要研究成果,个人觉得也值得一读。


简述

这篇文章首先介绍了深度学习的基本前期储备知识、发展背景,并对机器学习范畴内一个重要方向——监督学习进行完整介绍,然后介绍了反向传播算法和微积分链式法则等深度学习基础内容。
文章的接下来重点介绍了卷积神经网络CNN的实现过程、几个非常重要的经典卷积神经网络以及深度卷积神经网络对于视觉任务理解的应用。
文章最后探讨了分布表示和语言模型,循环神经网络RNN原理以及对未来的展望和现实的实现。
总而言之,我觉得这是一篇值得逐字逐句反复阅读咀嚼的文章,读完这篇文章,大概就相当于打开了深度学习的大门了吧。
这篇文章的个人理解与感悟或许我以后会补上,在接触还不深的情况下我不说废话啦,先附上原文,其中黄色高亮部分是一些比较重要的内容,大家有时间的话可以认真看一下。
下面附上原文链接
最后贴一张我觉得挺搞笑的图。

这张图片还有张兄弟图,可以看看我的另一篇论文分享artificial-intelligence笔记:人工智能前沿发展情况分享


碰到底线咯 后面没有啦

本文标题:deep learning笔记:一篇非常经典的论文——NatureDeepReview

文章作者:高深远

发布时间:2019年09月14日 - 14:25

最后更新:2020年02月16日 - 07:14

原始链接:https://gsy00517.github.io/deep-learning20190914142553/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:一篇非常经典的论文——NatureDeepReview | 高深远的博客

deep learning笔记:一篇非常经典的论文——NatureDeepReview

这是一篇非常经典的有关深度学习的论文,最近在看一个网课的时候又被提到了,因此特地找了pdf文档放在这里和大家分享。之后还分享了一篇deep-learning笔记:近年来深度学习的重要研究成果,个人觉得也值得一读。


简述

这篇文章首先介绍了深度学习的基本前期储备知识、发展背景,并对机器学习范畴内一个重要方向——监督学习进行完整介绍,然后介绍了反向传播算法和微积分链式法则等深度学习基础内容。
文章的接下来重点介绍了卷积神经网络CNN的实现过程、几个非常重要的经典卷积神经网络以及深度卷积神经网络对于视觉任务理解的应用。
文章最后探讨了分布表示和语言模型,循环神经网络RNN原理以及对未来的展望和现实的实现。
总而言之,我觉得这是一篇值得逐字逐句反复阅读咀嚼的文章,读完这篇文章,大概就相当于打开了深度学习的大门了吧。
这篇文章的个人理解与感悟或许我以后会补上,在接触还不深的情况下我不说废话啦,先附上原文,其中黄色高亮部分是一些比较重要的内容,大家有时间的话可以认真看一下。
下面附上原文链接
最后贴一张我觉得挺搞笑的图。

这张图片还有张兄弟图,可以看看我的另一篇论文分享artificial-intelligence笔记:人工智能前沿发展情况分享


碰到底线咯 后面没有啦

本文标题:deep learning笔记:一篇非常经典的论文——NatureDeepReview

文章作者:高深远

发布时间:2019年09月14日 - 14:25

最后更新:2020年02月16日 - 07:14

原始链接:https://gsy00517.github.io/deep-learning20190914142553/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:着眼于深度——VGG简介与pytorch实现 | 高深远的博客

deep learning笔记:着眼于深度——VGG简介与pytorch实现

VGG是我第一个自己编程实践的卷积神经网络,也是挺高兴的,下面我就对VGG在这篇文章中做一个分享。

References

电子文献:
https://blog.csdn.net/xiaohuihui1994/article/details/89207534
https://blog.csdn.net/sinat_33487968/article/details/83584289
https://blog.csdn.net/qq_32172681/article/details/95971492

参考文献:
[1]Very Deep Convolutional Networks For Large-scale Image Recognition


简介

VGG模型在2014年取得了ILSVRC竞赛的第二名,第一名是GoogLeNet。但是VGG在多个迁移学习任务中的表现要优于GoogleNet。

相比之前的神经网络,VGG主要有两大进步:其一是它增加了深度,其二是它使用了小的3x3的卷积核,这可以使它在增加深度的时候一定程度上防止了参数的增长。缺点是它的参数量比较庞大,但这并不意味着它不值得我们仔细研究。下图展示的是VGG的结构。

为了通过对比来对VGG的一些改进进行解释,VGG的作者在论文中提供了多个版本。


论文

要详细分析VGG,我可能不能像网上写的那样好,更不可能像论文一样明白。那么我在这里就先附上论文。
论文原版
论文中文版
我在英文原版中用黄颜色高亮了我觉得比较重要的内容,大家可以参考一下。
大家也可以自己到网上进行搜索,这类经典的网络网上有许多介绍与分析。
通过论文或者网上的资源对这个网络有一定理解之后,你可以看看我下面的代码实现。


结论

此篇论文的得出了一些结论,总结如下:

  1. 在一定范围内,通过增加网络深度能有效地提升网络性能。这在ResNet里更是得到了显著地体现,上面的ILSVRC历年winner表现统计图就是一个很好的证明,可参见deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现
  2. 与AlexNet对比可知,多个小卷积核比单个大卷积核性能要好。
  3. AlexNet中用到的LRN层(局部响应归一化层)并没有带来性能的提升,因此可以排除。
  4. 尺度抖动(scale jittering)即多尺度训练、多尺度测试有利于网络性能的提升。
  5. 最佳模型为VGG16,其从头到尾只用了3x3的卷积和2x2的池化。

特点

VGG的特点(创新点)主要有如下四个:

  1. 小卷积核

    VGG使用多个小卷积核来代替大的,这样一方面可以减少参数,另一方面相当于进行了更多的非线性映射,可以增加网络的拟合、表达能力。
  2. 小池化核

    相比AlexNet的3x3池化核,VGG一律采用了2x2的池化核。
  3. 层数更深

    若仅计算conv、fc层的话,VGG中常用的网络层数达到了16、19层(VGG16效果最好),这相较于前几年的研究是一个深度的提升。
  4. conv替代fc

    在基本的CNN中,全连接层的作用是将经过多个卷积层和池化层的图像特征图中的特征进行整合,获取图像特征具有的高层含义,用于图像分类。
    如果我们把全连接层的输出不再看成n个节点的集合,而是视作一个1x1xn的输出层,那么我们就可以用卷积层来替换全连接层了。并且从数学角度看,它和全连接层是一样的。
    因为卷积层没有全连接层对输入的限制,因此使用卷积层代替全连接层可以接收任意宽或高的输入。
    此外,相对于全连接层而言,使用卷积层不会破坏图像的空间结构。这也是一些的网络使用1x1的全卷积层代替全连接层的重要原因。

感受野

这里会涉及一个名为感受野(Receptive Field)的概念,它指的是卷积神经网络每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域大小。简而言之,就是特征图上的一点跟原有图上有关系的点的区域。一般取一个pixel为单位,而输入的感受野就是1即只对应其自身的那个像素。画图易知,两层3x3的卷积层所得到的感受野与一层5x5的卷积层的感受野相同,这也是VGG使用3x3小卷积核来代替的原理之一。
感受野是CNN中一个比较重要的概念,一些目标检测的流行算法如SSD、Faster Rcnn等中的prior box和anchor box的设计都是以感受野为依据的。可以看一下computer-vision笔记:anchor-box


1x1卷积核

虽然VGG所用的是2x2的卷积核,但是在上文提到了一些网络使用1x1的全卷积层代替全连接层,那么顺便就对1x1卷积核的作用做一个总结。

  1. 如上文所述,使用卷积层就没有全连接层对输入尺寸的限制,这也方便了许多。
  2. 全连接层会改变网络的空间结构,卷积层不会破坏图像的空间结构。一般要学习是相邻的边界等特征,而全连接毫无目的地将整张图“全连接”,换言之全连接输出的是一维向量,势必将丢失大量二维信息,这显然是不合适的。
  3. 可以用于为决策增加非线性因素。
  4. 一些模型用1x1的全连接层来调整网络维度。比如MobileNet使用1x1的卷积核来扩维,GoogleNet、ResNet使用1x1的卷积核来降维。这里的降维类似于压缩处理,并不会影响训练结果,而1x1的卷积核可以使网络变薄,可以成倍地减少计算量。如下图所示,如果我们使用naive的inception,那么最后concatenate出来的特征图厚度会很大,而添加上1x1的卷积核之后就可以调整厚度了。

自己实现

这里我使用pytorch框架来实现VGG。pytorch是一个相对较新的框架,但热度上升很快。根据网上的介绍,pytorch是一个非常适合于学习与科研的深度学习框架。我尝试了之后,也发现上手很快。
在pytorch中,神经网络可以通过torch.nn包来构建。这里我不一一介绍了,大家可以参考pytorch官方中文教程来学习,照着文档自己动手敲一遍之后,基本上就知道了pytorch如何使用了。
为了方便直观的理解,我先提供一个VGG16版本的流程图。

下面是我实现VGG19版本的代码:
首先,我们import所需的包。

1
2
import torch
import torch.nn as nn

接下来,我们定义神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class VGG(nn.Module):
def __init__(self, num_classes = 1000): #imagenet图像库总共1000个类
super(VGG, self).__init__() #先运行父类nn.Module初始化函数

self.conv1_1 = nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 3, padding = 1)
#定义图像卷积函数:输入为图像(3个频道,即RGB图),输出为64张特征图,卷积核为3x3正方形,为保留原空间分辨率,卷积层的空间填充为1即padding等于1,也就是防止每次卷积尺寸缩小过快导致无法使用更多的卷积层
self.conv1_2 = nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, padding = 1)

self.conv2_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, padding = 1)
self.conv2_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, padding = 1)

self.conv3_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_3 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_4 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)

self.conv4_1 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_3 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_4 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)

self.conv5_1 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_3 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_4 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)

self.relu = nn.ReLU(inplace = True) #inplace=TRUE表示原地操作
self.max = nn.MaxPool2d(kernel_size = 2, stride = 2)

self.fc1 = nn.Linear(512 * 7 * 7, 4096) #定义全连接函数1为线性函数:y = Wx + b,并将512*7*7个节点连接到4096个节点上
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, num_classes)
#定义全连接函数3为线性函数:y = Wx + b,并将4096个节点连接到num_classes个节点上,然后可用softmax进行处理

#定义该神经网络的向前传播函数,该函数必须定义,一旦定义成功,向后传播函数也会自动生成(autograd)
def forward(self, x):

x = self.relu(self.conv1_1(x))
x = self.relu(self.conv1_2(x))
x = self.max(x)
#输入x经过卷积之后,经过激活函数ReLU,循环两次,最后使用2x2的窗口进行最大池化Max pooling,然后更新到x

x = self.relu(self.conv2_1(x))
x = self.relu(self.conv2_2(x))
x = self.max(x)

x = self.relu(self.conv3_1(x))
x = self.relu(self.conv3_2(x))
x = self.relu(self.conv3_3(x))
x = self.relu(self.conv3_4(x))
x = self.max(x)

x = self.relu(self.conv4_1(x))
x = self.relu(self.conv4_2(x))
x = self.relu(self.conv4_3(x))
x = self.relu(self.conv4_4(x))
x = self.max(x)

x = self.relu(self.conv5_1(x))
x = self.relu(self.conv5_2(x))
x = self.relu(self.conv5_3(x))
x = self.relu(self.conv5_4(x))
x = self.max(x)

x = x.view(-1, self.num_flat_features(x)) #view函数将张量x变形成一维的向量形式,总特征数并不改变,为接下来的全连接作准备

x = self.fc1(x) #输入x经过全连接1,然后更新x
x = self.fc2(x)
x = self.fc3(x)
return x

def num_flat_features(self, x):
size = x.size()[1:] #all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features

vgg = VGG()
print(vgg)

我们print网络,可以看到输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VGG(
(conv1_1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv1_2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2_1): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2_2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_1): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_4): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_1): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_4): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_4): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(relu): ReLU(inplace=True)
(max): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(fc1): Linear(in_features=25088, out_features=4096, bias=True)
(fc2): Linear(in_features=4096, out_features=4096, bias=True)
(fc3): Linear(in_features=4096, out_features=1000, bias=True)
)

最后我们随机生成一个张量来进行验证。

1
2
3
input = torch.randn(1, 3, 224, 224)
out = vgg(input)
print(out)

其中(1, 3, 224, 224)表示1个3x224x224的矩阵,这是因为VGG输入的是固定尺寸的224x224的RGB(三通道)图像。
如果没有报错,那么就说明你的神经网络成功运行通过了。
我们也可以使用torch.nn.functional来实现激活函数与池化层,这样的话,你需要还需要多引入一个包:

1
2
3
import torch
import torch.nn as nn
import torch.nn.functional as F #新增

同时,你不需要在init中实例化激活函数与最大池化层,相应的,你需要对forward前馈函数进行更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def forward(self, x):

x = F.relu(self.conv1_1(x))
x = F.relu(self.conv1_2(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)
#输入x经过卷积之后,经过激活函数ReLU,循环两次,最后使用2x2的窗口进行最大池化Max pooling,然后更新到x

x = F.relu(self.conv2_1(x))
x = F.relu(self.conv2_2(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv3_1(x))
x = F.relu(self.conv3_2(x))
x = F.relu(self.conv3_3(x))
x = F.relu(self.conv3_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv4_1(x))
x = F.relu(self.conv4_2(x))
x = F.relu(self.conv4_3(x))
x = F.relu(self.conv4_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv5_1(x))
x = F.relu(self.conv5_2(x))
x = F.relu(self.conv5_3(x))
x = F.relu(self.conv5_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = x.view(-1, self.num_flat_features(x)) #view函数将张量x变形成一维的向量形式,总特征数并不改变,为接下来的全连接作准备

x = self.fc1(x) #输入x经过全连接1,然后更新x
x = self.fc2(x)
x = self.fc3(x)
return x

如果你之前运行通过的话,那么这里也是没有问题的。
这里我想说明一下torch.nntorch.nn.functional的区别。
这两个包中有许多类似的激活函数与损失函数,但是它们又有如下不同:
首先,在定义函数层(继承nn.Module)时,init函数中应该用torch.nn,例如torch.nn.ReLUtorch.nn.Dropout2d,而forward中应该用torch.nn.functional,例如torch.nn.functional.relu,不过请注意,init里面定义的是标准的网络层。只有torch.nn定义的才会进行训练。torch.nn.functional定义的需要自己手动设置参数。所以通常,激活函数或者卷积之类的都用torch.nn定义。
另外,torch.nn是类,必须要先在init中实例化,然后在forward中使用,而torch.nn.functional可以直接在forward中使用。
大家还可以通过官方文档torch.nn.functionaltorch.nn来进一步了解两者的区别。
大家或许发现,我的代码中有大量的重复性工作。是的,你将在文章后面的官方实现中看到优化的代码,但是相对来说,我的代码更加直观些,完全是按照网络的结构顺序从上到下编写的,可以方便初学者(including myself)的理解。


出现的问题

虽然我的代码比较简单直白,但是过程中并不是一帆风顺的,出现了两次报错:

  1. 输入输出不匹配

    当我第一遍运行时,出现了一个RuntimeError: 这是一个超级低级的错误,经学长提醒后我才发现,我两个卷积层之间输出输入的channel数并不匹配: 唉又是ctrl+C+V惹的祸,改正后的网络可以参见上文。
    在这里,我想提醒我自己和大家注意一下卷积层输入输出的维度公式:
    假设输入的宽、高记为W、H。
    超参数中,卷积核的维度是F,stride步长是S,padding是P。
    那么输出的宽X与高Y可用如下公式表示:然而,当我在计算ResNet的维度的时候,发现套用这个公式是除不尽的。于是我搜索到了如下规则:
    1. 对卷积层操作,除不尽时,向下取整。
    2. 对池化层操作,除不尽时,向上取整。
  2. 没有把张量转化成一维向量

    上面的问题解决了,结果还有错误:

    根据报错,可以发现3584x7是等于25088的,结合pytorch官方文档,我意识到我在把张量输入全连接层时,没有把它拍扁成一维。因此,我按照官方文档添加了如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    x = x.view(-1, self.num_flat_features(x))

    def num_flat_features(self, x):
    size = x.size()[1:] #all dimensions except the batch dimension
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    再运行,问题解决。
    注意这里的“except the batch dimension”,我在后面deep-learning笔记:记首次ResNet实战中就踩了坑。

另外,我原本写的代码中,在卷积层之间的对应位置都加上了relu激活函数与池化层。后来我才意识到,由于它们不具有任何需要学习的参数,我可以直接把它们拿出来单独定义:

1
2
self.relu = nn.ReLU(inplace = True)
self.max = nn.MaxPool2d(kernel_size = 2, stride = 2)

虽然是一些很低级的坑,但我还是想写下来供我自己和大家今后参考。


官方源码

由于VGG的结构设计非常有规律,因此官方源码给出了更简洁的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch.nn as nn
import math

class VGG(nn.Module):

def __init__(self, features, num_classes=1000, init_weights=True):
super(VGG, self).__init__()
self.features = features
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
if init_weights:
self._initialize_weights()

def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x

def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
m.weight.data.normal_(0, 0.01)
m.bias.data.zero_()

因为VGG中卷积层的重复性比较高,所以官方使用一个函数来循环产生卷积层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def make_layers(cfg, batch_norm=False):
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)

接下来定义各个版本的卷积层(可参考上文中对论文的截图),这里的“M”表示的是最大池化层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
cfg = {
'A': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'B': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'E': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

def vgg11(**kwargs):
model = VGG(make_layers(cfg['A']), **kwargs)
return model

def vgg11_bn(**kwargs):
model = VGG(make_layers(cfg['A'], batch_norm=True), **kwargs)
return model

def vgg13(**kwargs):
model = VGG(make_layers(cfg['B']), **kwargs)
return model

def vgg13_bn(**kwargs):
model = VGG(make_layers(cfg['B'], batch_norm=True), **kwargs)
return model

def vgg16(**kwargs):
model = VGG(make_layers(cfg['D']), **kwargs)
return model

def vgg16_bn(**kwargs):
model = VGG(make_layers(cfg['D'], batch_norm=True), **kwargs)
return model

def vgg19(**kwargs):
model = VGG(make_layers(cfg['E']), **kwargs)
return model

def vgg19_bn(**kwargs):
model = VGG(make_layers(cfg['E'], batch_norm=True), **kwargs)
return model

if __name__ == '__main__':
# 'VGG', 'vgg11', 'vgg11_bn', 'vgg13', 'vgg13_bn', 'vgg16', 'vgg16_bn', 'vgg19_bn', 'vgg19'
# Example
net11 = vgg11()
print(net11)

附上pytorch官方源码链接。可以在vision/torchvision/models/下找到一系列用pytorch实现的经典神经网络模型。
好了,以上就是VGG的介绍与实现,如有不足之处欢迎大家补充!


碰到底线咯 后面没有啦

本文标题:deep learning笔记:着眼于深度——VGG简介与pytorch实现

文章作者:高深远

发布时间:2019年09月15日 - 07:38

最后更新:2020年02月15日 - 22:10

原始链接:https://gsy00517.github.io/deep-learning20190915073809/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:着眼于深度——VGG简介与pytorch实现 | 高深远的博客

deep learning笔记:着眼于深度——VGG简介与pytorch实现

VGG是我第一个自己编程实践的卷积神经网络,也是挺高兴的,下面我就对VGG在这篇文章中做一个分享。

References

电子文献:
https://blog.csdn.net/xiaohuihui1994/article/details/89207534
https://blog.csdn.net/sinat_33487968/article/details/83584289
https://blog.csdn.net/qq_32172681/article/details/95971492

参考文献:
[1]Very Deep Convolutional Networks For Large-scale Image Recognition


简介

VGG模型在2014年取得了ILSVRC竞赛的第二名,第一名是GoogLeNet。但是VGG在多个迁移学习任务中的表现要优于GoogleNet。

相比之前的神经网络,VGG主要有两大进步:其一是它增加了深度,其二是它使用了小的3x3的卷积核,这可以使它在增加深度的时候一定程度上防止了参数的增长。缺点是它的参数量比较庞大,但这并不意味着它不值得我们仔细研究。下图展示的是VGG的结构。

为了通过对比来对VGG的一些改进进行解释,VGG的作者在论文中提供了多个版本。


论文

要详细分析VGG,我可能不能像网上写的那样好,更不可能像论文一样明白。那么我在这里就先附上论文。
论文原版
论文中文版
我在英文原版中用黄颜色高亮了我觉得比较重要的内容,大家可以参考一下。
大家也可以自己到网上进行搜索,这类经典的网络网上有许多介绍与分析。
通过论文或者网上的资源对这个网络有一定理解之后,你可以看看我下面的代码实现。


结论

此篇论文的得出了一些结论,总结如下:

  1. 在一定范围内,通过增加网络深度能有效地提升网络性能。这在ResNet里更是得到了显著地体现,上面的ILSVRC历年winner表现统计图就是一个很好的证明,可参见deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现
  2. 与AlexNet对比可知,多个小卷积核比单个大卷积核性能要好。
  3. AlexNet中用到的LRN层(局部响应归一化层)并没有带来性能的提升,因此可以排除。
  4. 尺度抖动(scale jittering)即多尺度训练、多尺度测试有利于网络性能的提升。
  5. 最佳模型为VGG16,其从头到尾只用了3x3的卷积和2x2的池化。

特点

VGG的特点(创新点)主要有如下四个:

  1. 小卷积核

    VGG使用多个小卷积核来代替大的,这样一方面可以减少参数,另一方面相当于进行了更多的非线性映射,可以增加网络的拟合、表达能力。
  2. 小池化核

    相比AlexNet的3x3池化核,VGG一律采用了2x2的池化核。
  3. 层数更深

    若仅计算conv、fc层的话,VGG中常用的网络层数达到了16、19层(VGG16效果最好),这相较于前几年的研究是一个深度的提升。
  4. conv替代fc

    在基本的CNN中,全连接层的作用是将经过多个卷积层和池化层的图像特征图中的特征进行整合,获取图像特征具有的高层含义,用于图像分类。
    如果我们把全连接层的输出不再看成n个节点的集合,而是视作一个1x1xn的输出层,那么我们就可以用卷积层来替换全连接层了。并且从数学角度看,它和全连接层是一样的。
    因为卷积层没有全连接层对输入的限制,因此使用卷积层代替全连接层可以接收任意宽或高的输入。
    此外,相对于全连接层而言,使用卷积层不会破坏图像的空间结构。这也是一些的网络使用1x1的全卷积层代替全连接层的重要原因。

感受野

这里会涉及一个名为感受野(Receptive Field)的概念,它指的是卷积神经网络每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域大小。简而言之,就是特征图上的一点跟原有图上有关系的点的区域。一般取一个pixel为单位,而输入的感受野就是1即只对应其自身的那个像素。画图易知,两层3x3的卷积层所得到的感受野与一层5x5的卷积层的感受野相同,这也是VGG使用3x3小卷积核来代替的原理之一。
感受野是CNN中一个比较重要的概念,一些目标检测的流行算法如SSD、Faster Rcnn等中的prior box和anchor box的设计都是以感受野为依据的。可以看一下computer-vision笔记:anchor-box


1x1卷积核

虽然VGG所用的是2x2的卷积核,但是在上文提到了一些网络使用1x1的全卷积层代替全连接层,那么顺便就对1x1卷积核的作用做一个总结。

  1. 如上文所述,使用卷积层就没有全连接层对输入尺寸的限制,这也方便了许多。
  2. 全连接层会改变网络的空间结构,卷积层不会破坏图像的空间结构。一般要学习是相邻的边界等特征,而全连接毫无目的地将整张图“全连接”,换言之全连接输出的是一维向量,势必将丢失大量二维信息,这显然是不合适的。
  3. 可以用于为决策增加非线性因素。
  4. 一些模型用1x1的全连接层来调整网络维度。比如MobileNet使用1x1的卷积核来扩维,GoogleNet、ResNet使用1x1的卷积核来降维。这里的降维类似于压缩处理,并不会影响训练结果,而1x1的卷积核可以使网络变薄,可以成倍地减少计算量。如下图所示,如果我们使用naive的inception,那么最后concatenate出来的特征图厚度会很大,而添加上1x1的卷积核之后就可以调整厚度了。

自己实现

这里我使用pytorch框架来实现VGG。pytorch是一个相对较新的框架,但热度上升很快。根据网上的介绍,pytorch是一个非常适合于学习与科研的深度学习框架。我尝试了之后,也发现上手很快。
在pytorch中,神经网络可以通过torch.nn包来构建。这里我不一一介绍了,大家可以参考pytorch官方中文教程来学习,照着文档自己动手敲一遍之后,基本上就知道了pytorch如何使用了。
为了方便直观的理解,我先提供一个VGG16版本的流程图。

下面是我实现VGG19版本的代码:
首先,我们import所需的包。

1
2
import torch
import torch.nn as nn

接下来,我们定义神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class VGG(nn.Module):
def __init__(self, num_classes = 1000): #imagenet图像库总共1000个类
super(VGG, self).__init__() #先运行父类nn.Module初始化函数

self.conv1_1 = nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 3, padding = 1)
#定义图像卷积函数:输入为图像(3个频道,即RGB图),输出为64张特征图,卷积核为3x3正方形,为保留原空间分辨率,卷积层的空间填充为1即padding等于1,也就是防止每次卷积尺寸缩小过快导致无法使用更多的卷积层
self.conv1_2 = nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, padding = 1)

self.conv2_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, padding = 1)
self.conv2_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, padding = 1)

self.conv3_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_3 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)
self.conv3_4 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1)

self.conv4_1 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_3 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv4_4 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)

self.conv5_1 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_3 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)
self.conv5_4 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1)

self.relu = nn.ReLU(inplace = True) #inplace=TRUE表示原地操作
self.max = nn.MaxPool2d(kernel_size = 2, stride = 2)

self.fc1 = nn.Linear(512 * 7 * 7, 4096) #定义全连接函数1为线性函数:y = Wx + b,并将512*7*7个节点连接到4096个节点上
self.fc2 = nn.Linear(4096, 4096)
self.fc3 = nn.Linear(4096, num_classes)
#定义全连接函数3为线性函数:y = Wx + b,并将4096个节点连接到num_classes个节点上,然后可用softmax进行处理

#定义该神经网络的向前传播函数,该函数必须定义,一旦定义成功,向后传播函数也会自动生成(autograd)
def forward(self, x):

x = self.relu(self.conv1_1(x))
x = self.relu(self.conv1_2(x))
x = self.max(x)
#输入x经过卷积之后,经过激活函数ReLU,循环两次,最后使用2x2的窗口进行最大池化Max pooling,然后更新到x

x = self.relu(self.conv2_1(x))
x = self.relu(self.conv2_2(x))
x = self.max(x)

x = self.relu(self.conv3_1(x))
x = self.relu(self.conv3_2(x))
x = self.relu(self.conv3_3(x))
x = self.relu(self.conv3_4(x))
x = self.max(x)

x = self.relu(self.conv4_1(x))
x = self.relu(self.conv4_2(x))
x = self.relu(self.conv4_3(x))
x = self.relu(self.conv4_4(x))
x = self.max(x)

x = self.relu(self.conv5_1(x))
x = self.relu(self.conv5_2(x))
x = self.relu(self.conv5_3(x))
x = self.relu(self.conv5_4(x))
x = self.max(x)

x = x.view(-1, self.num_flat_features(x)) #view函数将张量x变形成一维的向量形式,总特征数并不改变,为接下来的全连接作准备

x = self.fc1(x) #输入x经过全连接1,然后更新x
x = self.fc2(x)
x = self.fc3(x)
return x

def num_flat_features(self, x):
size = x.size()[1:] #all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features

vgg = VGG()
print(vgg)

我们print网络,可以看到输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
VGG(
(conv1_1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv1_2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2_1): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2_2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_1): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv3_4): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_1): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv4_4): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_3): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv5_4): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(relu): ReLU(inplace=True)
(max): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(fc1): Linear(in_features=25088, out_features=4096, bias=True)
(fc2): Linear(in_features=4096, out_features=4096, bias=True)
(fc3): Linear(in_features=4096, out_features=1000, bias=True)
)

最后我们随机生成一个张量来进行验证。

1
2
3
input = torch.randn(1, 3, 224, 224)
out = vgg(input)
print(out)

其中(1, 3, 224, 224)表示1个3x224x224的矩阵,这是因为VGG输入的是固定尺寸的224x224的RGB(三通道)图像。
如果没有报错,那么就说明你的神经网络成功运行通过了。
我们也可以使用torch.nn.functional来实现激活函数与池化层,这样的话,你需要还需要多引入一个包:

1
2
3
import torch
import torch.nn as nn
import torch.nn.functional as F #新增

同时,你不需要在init中实例化激活函数与最大池化层,相应的,你需要对forward前馈函数进行更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def forward(self, x):

x = F.relu(self.conv1_1(x))
x = F.relu(self.conv1_2(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)
#输入x经过卷积之后,经过激活函数ReLU,循环两次,最后使用2x2的窗口进行最大池化Max pooling,然后更新到x

x = F.relu(self.conv2_1(x))
x = F.relu(self.conv2_2(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv3_1(x))
x = F.relu(self.conv3_2(x))
x = F.relu(self.conv3_3(x))
x = F.relu(self.conv3_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv4_1(x))
x = F.relu(self.conv4_2(x))
x = F.relu(self.conv4_3(x))
x = F.relu(self.conv4_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = F.relu(self.conv5_1(x))
x = F.relu(self.conv5_2(x))
x = F.relu(self.conv5_3(x))
x = F.relu(self.conv5_4(x))
x = F.max_pool2d(x, kernel_size = 2, stride = 2)

x = x.view(-1, self.num_flat_features(x)) #view函数将张量x变形成一维的向量形式,总特征数并不改变,为接下来的全连接作准备

x = self.fc1(x) #输入x经过全连接1,然后更新x
x = self.fc2(x)
x = self.fc3(x)
return x

如果你之前运行通过的话,那么这里也是没有问题的。
这里我想说明一下torch.nntorch.nn.functional的区别。
这两个包中有许多类似的激活函数与损失函数,但是它们又有如下不同:
首先,在定义函数层(继承nn.Module)时,init函数中应该用torch.nn,例如torch.nn.ReLUtorch.nn.Dropout2d,而forward中应该用torch.nn.functional,例如torch.nn.functional.relu,不过请注意,init里面定义的是标准的网络层。只有torch.nn定义的才会进行训练。torch.nn.functional定义的需要自己手动设置参数。所以通常,激活函数或者卷积之类的都用torch.nn定义。
另外,torch.nn是类,必须要先在init中实例化,然后在forward中使用,而torch.nn.functional可以直接在forward中使用。
大家还可以通过官方文档torch.nn.functionaltorch.nn来进一步了解两者的区别。
大家或许发现,我的代码中有大量的重复性工作。是的,你将在文章后面的官方实现中看到优化的代码,但是相对来说,我的代码更加直观些,完全是按照网络的结构顺序从上到下编写的,可以方便初学者(including myself)的理解。


出现的问题

虽然我的代码比较简单直白,但是过程中并不是一帆风顺的,出现了两次报错:

  1. 输入输出不匹配

    当我第一遍运行时,出现了一个RuntimeError: 这是一个超级低级的错误,经学长提醒后我才发现,我两个卷积层之间输出输入的channel数并不匹配: 唉又是ctrl+C+V惹的祸,改正后的网络可以参见上文。
    在这里,我想提醒我自己和大家注意一下卷积层输入输出的维度公式:
    假设输入的宽、高记为W、H。
    超参数中,卷积核的维度是F,stride步长是S,padding是P。
    那么输出的宽X与高Y可用如下公式表示:然而,当我在计算ResNet的维度的时候,发现套用这个公式是除不尽的。于是我搜索到了如下规则:
    1. 对卷积层操作,除不尽时,向下取整。
    2. 对池化层操作,除不尽时,向上取整。
  2. 没有把张量转化成一维向量

    上面的问题解决了,结果还有错误:

    根据报错,可以发现3584x7是等于25088的,结合pytorch官方文档,我意识到我在把张量输入全连接层时,没有把它拍扁成一维。因此,我按照官方文档添加了如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    x = x.view(-1, self.num_flat_features(x))

    def num_flat_features(self, x):
    size = x.size()[1:] #all dimensions except the batch dimension
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    再运行,问题解决。
    注意这里的“except the batch dimension”,我在后面deep-learning笔记:记首次ResNet实战中就踩了坑。

另外,我原本写的代码中,在卷积层之间的对应位置都加上了relu激活函数与池化层。后来我才意识到,由于它们不具有任何需要学习的参数,我可以直接把它们拿出来单独定义:

1
2
self.relu = nn.ReLU(inplace = True)
self.max = nn.MaxPool2d(kernel_size = 2, stride = 2)

虽然是一些很低级的坑,但我还是想写下来供我自己和大家今后参考。


官方源码

由于VGG的结构设计非常有规律,因此官方源码给出了更简洁的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch.nn as nn
import math

class VGG(nn.Module):

def __init__(self, features, num_classes=1000, init_weights=True):
super(VGG, self).__init__()
self.features = features
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
if init_weights:
self._initialize_weights()

def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x

def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
m.weight.data.normal_(0, 0.01)
m.bias.data.zero_()

因为VGG中卷积层的重复性比较高,所以官方使用一个函数来循环产生卷积层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def make_layers(cfg, batch_norm=False):
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)

接下来定义各个版本的卷积层(可参考上文中对论文的截图),这里的“M”表示的是最大池化层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
cfg = {
'A': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'B': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'E': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

def vgg11(**kwargs):
model = VGG(make_layers(cfg['A']), **kwargs)
return model

def vgg11_bn(**kwargs):
model = VGG(make_layers(cfg['A'], batch_norm=True), **kwargs)
return model

def vgg13(**kwargs):
model = VGG(make_layers(cfg['B']), **kwargs)
return model

def vgg13_bn(**kwargs):
model = VGG(make_layers(cfg['B'], batch_norm=True), **kwargs)
return model

def vgg16(**kwargs):
model = VGG(make_layers(cfg['D']), **kwargs)
return model

def vgg16_bn(**kwargs):
model = VGG(make_layers(cfg['D'], batch_norm=True), **kwargs)
return model

def vgg19(**kwargs):
model = VGG(make_layers(cfg['E']), **kwargs)
return model

def vgg19_bn(**kwargs):
model = VGG(make_layers(cfg['E'], batch_norm=True), **kwargs)
return model

if __name__ == '__main__':
# 'VGG', 'vgg11', 'vgg11_bn', 'vgg13', 'vgg13_bn', 'vgg16', 'vgg16_bn', 'vgg19_bn', 'vgg19'
# Example
net11 = vgg11()
print(net11)

附上pytorch官方源码链接。可以在vision/torchvision/models/下找到一系列用pytorch实现的经典神经网络模型。
好了,以上就是VGG的介绍与实现,如有不足之处欢迎大家补充!


碰到底线咯 后面没有啦

本文标题:deep learning笔记:着眼于深度——VGG简介与pytorch实现

文章作者:高深远

发布时间:2019年09月15日 - 07:38

最后更新:2020年02月15日 - 22:10

原始链接:https://gsy00517.github.io/deep-learning20190915073809/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:开启深度学习热潮——AlexNet | 高深远的博客

deep learning笔记:开启深度学习热潮——AlexNet

继之前那篇deep-learning笔记:着眼于深度——VGG简介与pytorch实现,我觉得还是有必要提一下VGG的前辈——具有历史意义的AlexNet,于是就写了这篇文章简要介绍一下。

References

电子文献:
https://blog.csdn.net/zym19941119/article/details/78982441

参考文献:
[1]ImageNet Classification with Deep Convolutional Neural Networks


简介

ALexNet是第一个运用大型深度卷积神经网络的模型,在ILSVRC中一下子比前一年把错误率降低了10%,这是非常惊人的,也很快引起了注意。于是,自2012年开始,深度学习热潮由此引发。

根据我之前听网课的笔记以及网上的其他文章,我把AlexNet主要的进步归纳如下:

  1. 使用大型深度卷积神经网络。
  2. 分组卷积(groupconvolution)来充分利用GPU。
  3. 随机失活dropout:一种有效的正则化方法。关于正则化,可以看我的博文machine-learning笔记:机器学习中正则化的理解
  4. 数据增强data augumentation:增大数据集以减小过拟合问题。
  5. ReLU激活函数:即max(0,x),至今还被广泛应用。

个人思考

这段时间也看了不少东西,对于如何提升神经网络的性能这个问题,我觉得主要有如下三个方面:

  1. 从网络本身入手

    1. 增加深度。
    2. 增加宽度。
    3. 减少参数量。
    4. 防止过拟合。
    5. 解决梯度消失的问题。
  2. 从数据集入手

    1. 尽可能使用多的数据。
  3. 从硬件入手

    1. 提升GPU性能。
    2. 充分利用现有的GPU性能。
      当你阅读完AlexNet的论文,你会发现它在这几个方面都有思考且做出了非常优秀的改进。

论文

在放论文之前,我还是先贴一张流程图,方便在阅读论文的时候进行对照与理解。

下面奉上宝贵的论文:
论文原版
论文中文版
从introduction第一句开始,作者就开始了一段长长的吐槽:
Current approaches to object recognition make essential use of machine learning methods…
吐槽Yann LeCun大佬的论文被顶会拒收了仅仅因为Yann LeCun使用了神经网络。其实,那段时间之前,由于SVM等机器学习方法的兴起,神经网络是一种被许多ML大佬们看不起的算法模型。
在introduction的最后,作者留下了这样一句经典的话:
All of our experiments suggest that our results can be improved simply by waiting for faster GPUs and bigger datasets to become available.
有没有感觉到一种新世界大门被打开的感觉呢?
有关论文别的内容,我暂不多说了,大家可以自己看论文学习与体会。
附上推荐重点阅读的章节:3.1 ReLU Nonlinearity;3.5 Overall Architecture;4 Reducing Overfitting。


说明

同我写VGG的那篇文章中一样,我在英文原版中用黄颜色高亮了我觉得重要的内容给自己和大家今后参考。
另外,我在这里推荐大家还是先尝试阅读英文原版。一方面由于一些公式、符号以及名词的原因,英文原版叙述更精准,中文翻译有缺漏、偏颇之处;另一方面更重要的,接触这些方面的知识仅参考中文是远远不够的。
在这里我推荐一个chrome英文pdf阅读插件,大家可以自己到chrome里面搜索安装:


有了这个插件,遇到不认识的单词,只需双击单词,就可以看到中文释义,一定程度上可以保证阅读的流畅性。但是如果想从根本上解决问题,只有好好背单词吧(我也在朝这个方向努力…)。
另外,iPad的上也有好多强大的app,在这里不一一推荐了。

补充:最近又发现一款特别好用且美观的查词插件,功能非常强大,推荐一下:沙拉查词


碰到底线咯 后面没有啦

本文标题:deep learning笔记:开启深度学习热潮——AlexNet

文章作者:高深远

发布时间:2019年09月15日 - 11:38

最后更新:2020年02月15日 - 22:09

原始链接:https://gsy00517.github.io/deep-learning20190915113859/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:开启深度学习热潮——AlexNet | 高深远的博客

deep learning笔记:开启深度学习热潮——AlexNet

继之前那篇deep-learning笔记:着眼于深度——VGG简介与pytorch实现,我觉得还是有必要提一下VGG的前辈——具有历史意义的AlexNet,于是就写了这篇文章简要介绍一下。

References

电子文献:
https://blog.csdn.net/zym19941119/article/details/78982441

参考文献:
[1]ImageNet Classification with Deep Convolutional Neural Networks


简介

ALexNet是第一个运用大型深度卷积神经网络的模型,在ILSVRC中一下子比前一年把错误率降低了10%,这是非常惊人的,也很快引起了注意。于是,自2012年开始,深度学习热潮由此引发。

根据我之前听网课的笔记以及网上的其他文章,我把AlexNet主要的进步归纳如下:

  1. 使用大型深度卷积神经网络。
  2. 分组卷积(groupconvolution)来充分利用GPU。
  3. 随机失活dropout:一种有效的正则化方法。关于正则化,可以看我的博文machine-learning笔记:机器学习中正则化的理解
  4. 数据增强data augumentation:增大数据集以减小过拟合问题。
  5. ReLU激活函数:即max(0,x),至今还被广泛应用。

个人思考

这段时间也看了不少东西,对于如何提升神经网络的性能这个问题,我觉得主要有如下三个方面:

  1. 从网络本身入手

    1. 增加深度。
    2. 增加宽度。
    3. 减少参数量。
    4. 防止过拟合。
    5. 解决梯度消失的问题。
  2. 从数据集入手

    1. 尽可能使用多的数据。
  3. 从硬件入手

    1. 提升GPU性能。
    2. 充分利用现有的GPU性能。
      当你阅读完AlexNet的论文,你会发现它在这几个方面都有思考且做出了非常优秀的改进。

论文

在放论文之前,我还是先贴一张流程图,方便在阅读论文的时候进行对照与理解。

下面奉上宝贵的论文:
论文原版
论文中文版
从introduction第一句开始,作者就开始了一段长长的吐槽:
Current approaches to object recognition make essential use of machine learning methods…
吐槽Yann LeCun大佬的论文被顶会拒收了仅仅因为Yann LeCun使用了神经网络。其实,那段时间之前,由于SVM等机器学习方法的兴起,神经网络是一种被许多ML大佬们看不起的算法模型。
在introduction的最后,作者留下了这样一句经典的话:
All of our experiments suggest that our results can be improved simply by waiting for faster GPUs and bigger datasets to become available.
有没有感觉到一种新世界大门被打开的感觉呢?
有关论文别的内容,我暂不多说了,大家可以自己看论文学习与体会。
附上推荐重点阅读的章节:3.1 ReLU Nonlinearity;3.5 Overall Architecture;4 Reducing Overfitting。


说明

同我写VGG的那篇文章中一样,我在英文原版中用黄颜色高亮了我觉得重要的内容给自己和大家今后参考。
另外,我在这里推荐大家还是先尝试阅读英文原版。一方面由于一些公式、符号以及名词的原因,英文原版叙述更精准,中文翻译有缺漏、偏颇之处;另一方面更重要的,接触这些方面的知识仅参考中文是远远不够的。
在这里我推荐一个chrome英文pdf阅读插件,大家可以自己到chrome里面搜索安装:


有了这个插件,遇到不认识的单词,只需双击单词,就可以看到中文释义,一定程度上可以保证阅读的流畅性。但是如果想从根本上解决问题,只有好好背单词吧(我也在朝这个方向努力…)。
另外,iPad的上也有好多强大的app,在这里不一一推荐了。

补充:最近又发现一款特别好用且美观的查词插件,功能非常强大,推荐一下:沙拉查词


碰到底线咯 后面没有啦

本文标题:deep learning笔记:开启深度学习热潮——AlexNet

文章作者:高深远

发布时间:2019年09月15日 - 11:38

最后更新:2020年02月15日 - 22:09

原始链接:https://gsy00517.github.io/deep-learning20190915113859/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:学习率衰减与批归一化 | 高深远的博客

deep learning笔记:学习率衰减与批归一化

一段时间之前,在一个深度学习交流群里看到一个群友发问:为什么他的训练误差最后疯狂上下抖动而不是一直降低。

作为一个很萌的萌新,我当时也很疑惑。但后来我结合所学,仔细思考之后,发现这是一个挺容易犯的错误。

References

电子文献:
https://blog.csdn.net/bestrivern/article/details/86301619
https://www.jianshu.com/p/9643cba47655
https://www.cnblogs.com/eilearn/p/9780696.html
https://blog.csdn.net/donkey_1993/article/details/81871132
https://www.pytorchtutorial.com/how-to-use-batchnorm/


问题

事实上,这是一个在机器学习中就有可能遇到的问题,当学习速率α设置得过大时,往往在模型训练的后期难以达到最优解,而是在最优解附近来回抖动。还有可能反而使损失函数越来越大,甚至达到无穷,如下图所示。

而在深度学习中,假设我们使用mini-batch梯度下降法,由于mini-batch的数量不大,大概64或者128个样本,在迭代过程中会有噪声。这个时候使用固定的学习率导致的结果就是虽然下降朝向最小值,但不会精确地收敛,只会在附近不断地波动(蓝色线)。

但如果慢慢减少学习率,在初期,学习还是相对较快地,但随着学习率的变小,步伐也会变慢变小,所以最后当开始收敛时,你的曲线(绿色线)会在最小值附近的一个较小区域之内摆动,而不是在训练过程中,大幅度地在最小值附近摆动。

对于这个问题,我目前收集了有下面这些解决办法。


直接修改学习率

在吴恩达的机器学习课程中,他介绍了一种人为选择学习率的规则:每三倍选择一个学习率。
比如:我们首先选择了0.1为学习率,那么当这个学习率过大时,我们修改成0.3。倘若还是偏大,我们继续改为0.01、0.003、0.001…以此类推,当学习率偏小是也是以三倍增加并尝试检验,最终选出比较合适的学习率。
但这种方法只适用于模型数量小的情况,且这种方法终究还是固定的学习率,依旧无法很好地权衡从而达到前期快速下降与后期稳定收敛的目的。


学习率动态衰减

学习率衰减的本质在于,在学习初期,你能承受并且需要较大的步伐,但当开始收敛的时候,小一些的学习率能让你步伐小一些,从而更稳定地达到精确的最优解。
为此,我们另外增添衰减率超参数,构建函数使学习率能够在训练的过程中动态衰减。

其中decay rate称为衰减率,epoch num是代数,$ \alpha _{0} $是初始学习率。
此外还有下面这些构造方法:
指数衰减:$ \alpha =0.95^{epochnum}*\alpha _{0} $
其他常用方法:

其中k为mini-batch的数字。


几种衰减方法的实现

在pytorch中,学习率调整主要有两种方式:
1.直接修改optimizer中的lr参数。
2.利用lr_scheduler()提供的几种衰减函数。即使用torch.optim.lr_scheduler,基于循环的次数提供了一些方法来调节学习率。
3.利用torch.optim.lr_scheduler.ReduceLROnPlateau,基于验证测量结果来设置不同的学习率.
下面提供几种实现方法:
准备(对下列通用):

1
2
3
4
5
6
7
8
9
10
11
import torch
from torch.optim import * #包含Adam,lr_scheduler等
import torch.nn as nn

#生成一个简单全连接神经网络
class net(nn.Module):
def __init__(self):
super(net, self).__init__()
self.fc = nn.Linear(1, 10)
def forward(self, x):
return self.fc(x)

  1. 手动阶梯式衰减

    1
    2
    3
    4
    5
    6
    7
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    for epoch in range(100):
    if epoch % 5 == 0:
    for p in optimizer.param_groups:
    p['lr'] *= 0.9 #学习率超参的位置:optimizer.state_dict()['param_groups'][0]['lr']

    这里是每过5个epoch就进行一次衰减。

  2. lambda自定义衰减

    1
    2
    3
    4
    5
    6
    7
    8
    import numpy as np 
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    lambda1 = lambda epoch: np.sin(epoch) / epoch
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda = lambda1)
    for epoch in range(100):
    scheduler.step()

    lr_lambda会接收到一个int参数:epoch,然后根据epoch计算出对应的lr。如果设置多个lambda函数的话,会分别作用于optimizer中的不同的params_group。

  3. StepLR阶梯式衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.StepLR(optimizer, step_size = 5, gamma = 0.8)
    for epoch in range(100):
    scheduler.step()

    每个epoch,lr会自动乘以gamma。

  4. 三段式衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.MultiStepLR(optimizer, milestones = [20,80], gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是,当epoch进入milestones范围内即乘以gamma,离开milestones范围之后再乘以gamma。
    这种衰减方式也是在学术论文中最常见的方式,一般手动调整也会采用这种方法。

  5. 连续衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ExponentialLR(optimizer, gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是在每个epoch中lr都乘以gamma,从而达到连续衰减的效果。

  6. 余弦式调整

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max = 20)
    for epoch in range(100):
    scheduler.step()

    这里的T_max对应1/2个cos周期所对应的epoch数值。

  7. 基于loss和accuracy

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode = 'min', factor = 0.1, patience = 10, verbose = False, threshold = 0.0001, threshold_mode = 'rel', cooldown = 0, min_lr = 0, eps = 1e-08)
    for epoch in range(100):
    scheduler.step()

    当发现loss不再降低或者accuracy不再提高之后,就降低学习率。

    注:上面代码中各参数意义如下:
    mode:’min’模式检测metric是否不再减小,’max’模式检测metric是否不再增大;
    factor:触发条件后lr*=factor;
    patience:不再减小(或增大)的累计次数;
    verbose:触发条件后print;
    threshold:只关注超过阈值的显著变化;
    threshold_mode:有rel和abs两种阈值计算模式,rel规则:max模式下如果超过best(1+threshold)为显著,min模式下如果低于best(1-threshold)为显著;abs规则:max模式下如果超过best+threshold为显著,min模式下如果低于best-threshold为显著;
    cooldown:触发一次条件后,等待一定epoch再进行检测,避免lr下降过速;
    min_lr:最小的允许lr;
    eps:如果新旧lr之间的差异小与1e-8,则忽略此次更新。

这里非常感谢facebook的员工给我们提供了如此多的选择与便利!
对于上述方法如有任何疑惑,还请查阅torch.optim文档


批归一化(Batch Normalization)

除了对学习率进行调整之外,Batch Normalization也可以有效地解决之前的问题。
我是在学习ResNet的时候第一次遇到批归一化这个概念的。随着深度神经网络深度的加深,训练越来越困难,收敛越来越慢。为此,很多论文都尝试解决这个问题,比如ReLU激活函数,再比如Residual Network,而BN本质上也是解释并从某个不同的角度来解决这个问题的。
通过使用Batch Normalization,我们可以加快网络的收敛速度,这样我们就可以使用较大的学习率来训练网络了。此外,BN还提高了网络的泛化能力。
BN的基本思想其实相当直观:
首先,因为深层神经网络在做非线性变换前的激活输入值(就是x=WU+B,U是输入)随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,之所以训练收敛慢,一般是整体分布逐渐往非线性函数的取值区间的上下限两端靠近(对于Sigmoid函数来说,意味着激活输入值WU+B是大的负值或正值),这就导致了反向传播时低层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因。
事实上,神经网络学习过程本质上是为了学习数据的分布,而BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0、方差为1的标准正态分布,其实就是把越来越偏的分布强制拉回比较标准的分布,这样使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,从而让梯度变大,避免梯度消失问题产生,而且梯度变大意味着学习收敛速度快,因此通过BN能大大加快训练速度。
下面来看看BN的具体操作过程:

即以下四个步骤:
1.计算样本均值。
2.计算样本方差。
3.对样本数据进行标准化处理。
4.进行平移和缩放处理。这里引入了γ和β两个参数。通过训练可学习重构的γ和β这两个参数,让我们的网络可以学习恢复出原始网络所要学习的特征分布。
下面是BN层的训练流程:

这里的详细过程如下:
输入:待进入激活函数的变量。
输出:
1.这里的K,在卷积网络中可以看作是卷积核个数,如网络中第n层有64个卷积核,就需要计算64次。

注意:在正向传播时,会使用γ与β使得BN层输出与输入一样。

2.在反向传播时利用γ与β求得梯度从而改变训练权值(变量)。
3.通过不断迭代直到训练结束,求得关于不同层的γ与β。
4.不断遍历训练集中的图片,取出每个batch_size中的γ与β,最后统计每层BN的γ与β各自的和除以图片数量得到平均值,并对其做无偏估计直作为每一层的E[x]与Var[x]。
5.在预测的正向传播时,对测试数据求取γ与β,并使用该层的E[x]与Var[x],通过图中11:所表示的公式计算BN层输出。

注意:在预测时,BN层的输出已经被改变,因此BN层在预测中的作用体现在此处。

上面输入的是待进入激活函数的变量,在残差网络ResNet中,的确也是先经过BN层再用relu函数做非线性处理的。那么,为什么BN层一般用在线性层和卷积层的后面,而不是放在非线性单元即激活函数之后呢?
因为非线性单元的输出分布形状会在训练过程中变化,归一化无法消除他的方差偏移。相反的,全连接和卷积层的输出一般是一个对称、非稀疏的一个分布,更加类似高斯分布,对他们进行归一化会产生更加稳定的分布。
比如,我们对一个高斯分布的数据relu激活,那么小于0的直接就被抑制了,这样得到的结果很难是高斯分布了,这时候再添加一个BN层就很难达到所需的效果。
很多实验证明,BatchNorm只要用了就有效果,所以在一般情况下没有理由不用。但也有相反的情况,比如当每个batch里所有的sample都非常相似的时候,相似到mean和variance都基本为0时,最好不要用BatchNorm。此外如果batch size为1,从原理上来讲,此时用BatchNorm是没有任何意义的。

注意:通常我们在进行Transfer Learning的时候,会冻结之前的网络权重,注意这时候往往也会冻结BatchNorm中训练好的moving averages值。这些moving averages值只适用于以前的旧的数据,对新数据不一定适用。所以最好的方法是在Transfer Learning的时候不要冻结BatchNorm层,让moving averages值重新从新的数据中学习。


批归一化实现

这里还是使用pytorch进行实现。
准备(对下列通用):

1
2
import torch
import torch.nn as nn

  1. 2d或3d输入

    1
    2
    3
    4
    5
    6
    # 添加了可学习的仿射变换参数
    m = nn.BatchNorm1d(100)
    # 未添加可学习的仿射变换参数
    m = nn.BatchNorm1d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100))
    output = m(input)

    我们查看m,可以看到有如下形式:

    1
    BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=False, track_running_stats=True)

    这里解释一下涉及到的参数:
    num_features:来自期望输入的特征数,该期望输入的大小为:batch_size * num_features(* width)
    eps:为保证数值稳定性(分母不能趋近或取0),给分母加上的值,默认为1e-5。
    momentum:计算动态均值和动态方差并进行移动平均所使用的动量,默认为0.1。
    affine:一个布尔值,当设为true时,就给该层添加可学习的仿射变换参数。仿射变换将在后文做简单介绍。
    BatchNorm1d可以有两种输入输出:
    1.输入(N,C),输出(N,C)。
    2.输入(N,C,L),输出(N,C,L)。

  2. 3d或4d输入

    1
    2
    3
    4
    5
    m = nn.BatchNorm2d(100)
    #或者
    m = nn.BatchNorm2d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100, 35, 45))
    output = m(input)

    BatchNorm2d也可以有两种输入输出:
    1.输入(N,C,L),输出(N,C,L)。
    2.输入(N,C,H,W),输出(N,C,H,W)。

  3. 4d或5d输入

    1
    2
    3
    m = nn.BatchNorm3d(100)
    #或者
    m = nn.BatchNorm3d(100, affine=False)

    BatchNorm3d同样支持两种输入输出:
    1.输入(N,C,H,W),输出(N,C,H,W)。
    2.输入(N,C,D,H,W),输出(N,C,D,H,W)。


仿射变换

这里我简单介绍一下仿射变换的概念,仿射变换(Affine Transformation或Affine Map)是一种二维坐标(x, y)到二维坐标(u, v)的变换,它是另外两种简单变换的叠加,一是线性变换,二是平移变换。同时,仿射变换保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点确定一个唯一的仿射变换。

补充:
共线性:若几个点变换前在一条线上,则仿射变换后仍然在一条线上。
平行性:若两条线变换前平行,则变换后仍然平行。
共线比例不变性:变换前一条线上两条线段的比例,在变换后比例不变。

在二维图像变换中,它的一般表达如下:

可以视为线性变换R和平移变换T的叠加。
另外,仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切。因此我们可以将几种简单的变换矩阵相乘来实现仿射变换。


碰到底线咯 后面没有啦

本文标题:deep learning笔记:学习率衰减与批归一化

文章作者:高深远

发布时间:2019年10月01日 - 15:14

最后更新:2020年02月27日 - 07:46

原始链接:https://gsy00517.github.io/deep-learning20191001151454/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:学习率衰减与批归一化 | 高深远的博客

deep learning笔记:学习率衰减与批归一化

一段时间之前,在一个深度学习交流群里看到一个群友发问:为什么他的训练误差最后疯狂上下抖动而不是一直降低。

作为一个很萌的萌新,我当时也很疑惑。但后来我结合所学,仔细思考之后,发现这是一个挺容易犯的错误。

References

电子文献:
https://blog.csdn.net/bestrivern/article/details/86301619
https://www.jianshu.com/p/9643cba47655
https://www.cnblogs.com/eilearn/p/9780696.html
https://blog.csdn.net/donkey_1993/article/details/81871132
https://www.pytorchtutorial.com/how-to-use-batchnorm/


问题

事实上,这是一个在机器学习中就有可能遇到的问题,当学习速率α设置得过大时,往往在模型训练的后期难以达到最优解,而是在最优解附近来回抖动。还有可能反而使损失函数越来越大,甚至达到无穷,如下图所示。

而在深度学习中,假设我们使用mini-batch梯度下降法,由于mini-batch的数量不大,大概64或者128个样本,在迭代过程中会有噪声。这个时候使用固定的学习率导致的结果就是虽然下降朝向最小值,但不会精确地收敛,只会在附近不断地波动(蓝色线)。

但如果慢慢减少学习率,在初期,学习还是相对较快地,但随着学习率的变小,步伐也会变慢变小,所以最后当开始收敛时,你的曲线(绿色线)会在最小值附近的一个较小区域之内摆动,而不是在训练过程中,大幅度地在最小值附近摆动。

对于这个问题,我目前收集了有下面这些解决办法。


直接修改学习率

在吴恩达的机器学习课程中,他介绍了一种人为选择学习率的规则:每三倍选择一个学习率。
比如:我们首先选择了0.1为学习率,那么当这个学习率过大时,我们修改成0.3。倘若还是偏大,我们继续改为0.01、0.003、0.001…以此类推,当学习率偏小是也是以三倍增加并尝试检验,最终选出比较合适的学习率。
但这种方法只适用于模型数量小的情况,且这种方法终究还是固定的学习率,依旧无法很好地权衡从而达到前期快速下降与后期稳定收敛的目的。


学习率动态衰减

学习率衰减的本质在于,在学习初期,你能承受并且需要较大的步伐,但当开始收敛的时候,小一些的学习率能让你步伐小一些,从而更稳定地达到精确的最优解。
为此,我们另外增添衰减率超参数,构建函数使学习率能够在训练的过程中动态衰减。

其中decay rate称为衰减率,epoch num是代数,$ \alpha _{0} $是初始学习率。
此外还有下面这些构造方法:
指数衰减:$ \alpha =0.95^{epochnum}*\alpha _{0} $
其他常用方法:

其中k为mini-batch的数字。


几种衰减方法的实现

在pytorch中,学习率调整主要有两种方式:
1.直接修改optimizer中的lr参数。
2.利用lr_scheduler()提供的几种衰减函数。即使用torch.optim.lr_scheduler,基于循环的次数提供了一些方法来调节学习率。
3.利用torch.optim.lr_scheduler.ReduceLROnPlateau,基于验证测量结果来设置不同的学习率.
下面提供几种实现方法:
准备(对下列通用):

1
2
3
4
5
6
7
8
9
10
11
import torch
from torch.optim import * #包含Adam,lr_scheduler等
import torch.nn as nn

#生成一个简单全连接神经网络
class net(nn.Module):
def __init__(self):
super(net, self).__init__()
self.fc = nn.Linear(1, 10)
def forward(self, x):
return self.fc(x)

  1. 手动阶梯式衰减

    1
    2
    3
    4
    5
    6
    7
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    for epoch in range(100):
    if epoch % 5 == 0:
    for p in optimizer.param_groups:
    p['lr'] *= 0.9 #学习率超参的位置:optimizer.state_dict()['param_groups'][0]['lr']

    这里是每过5个epoch就进行一次衰减。

  2. lambda自定义衰减

    1
    2
    3
    4
    5
    6
    7
    8
    import numpy as np 
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    lambda1 = lambda epoch: np.sin(epoch) / epoch
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda = lambda1)
    for epoch in range(100):
    scheduler.step()

    lr_lambda会接收到一个int参数:epoch,然后根据epoch计算出对应的lr。如果设置多个lambda函数的话,会分别作用于optimizer中的不同的params_group。

  3. StepLR阶梯式衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.StepLR(optimizer, step_size = 5, gamma = 0.8)
    for epoch in range(100):
    scheduler.step()

    每个epoch,lr会自动乘以gamma。

  4. 三段式衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.MultiStepLR(optimizer, milestones = [20,80], gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是,当epoch进入milestones范围内即乘以gamma,离开milestones范围之后再乘以gamma。
    这种衰减方式也是在学术论文中最常见的方式,一般手动调整也会采用这种方法。

  5. 连续衰减

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ExponentialLR(optimizer, gamma = 0.9)
    for epoch in range(100):
    scheduler.step()

    这种方法就是在每个epoch中lr都乘以gamma,从而达到连续衰减的效果。

  6. 余弦式调整

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max = 20)
    for epoch in range(100):
    scheduler.step()

    这里的T_max对应1/2个cos周期所对应的epoch数值。

  7. 基于loss和accuracy

    1
    2
    3
    4
    5
    6
    model = net()
    LR = 0.01
    optimizer = Adam(model.parameters(), lr = LR)
    scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode = 'min', factor = 0.1, patience = 10, verbose = False, threshold = 0.0001, threshold_mode = 'rel', cooldown = 0, min_lr = 0, eps = 1e-08)
    for epoch in range(100):
    scheduler.step()

    当发现loss不再降低或者accuracy不再提高之后,就降低学习率。

    注:上面代码中各参数意义如下:
    mode:’min’模式检测metric是否不再减小,’max’模式检测metric是否不再增大;
    factor:触发条件后lr*=factor;
    patience:不再减小(或增大)的累计次数;
    verbose:触发条件后print;
    threshold:只关注超过阈值的显著变化;
    threshold_mode:有rel和abs两种阈值计算模式,rel规则:max模式下如果超过best(1+threshold)为显著,min模式下如果低于best(1-threshold)为显著;abs规则:max模式下如果超过best+threshold为显著,min模式下如果低于best-threshold为显著;
    cooldown:触发一次条件后,等待一定epoch再进行检测,避免lr下降过速;
    min_lr:最小的允许lr;
    eps:如果新旧lr之间的差异小与1e-8,则忽略此次更新。

这里非常感谢facebook的员工给我们提供了如此多的选择与便利!
对于上述方法如有任何疑惑,还请查阅torch.optim文档


批归一化(Batch Normalization)

除了对学习率进行调整之外,Batch Normalization也可以有效地解决之前的问题。
我是在学习ResNet的时候第一次遇到批归一化这个概念的。随着深度神经网络深度的加深,训练越来越困难,收敛越来越慢。为此,很多论文都尝试解决这个问题,比如ReLU激活函数,再比如Residual Network,而BN本质上也是解释并从某个不同的角度来解决这个问题的。
通过使用Batch Normalization,我们可以加快网络的收敛速度,这样我们就可以使用较大的学习率来训练网络了。此外,BN还提高了网络的泛化能力。
BN的基本思想其实相当直观:
首先,因为深层神经网络在做非线性变换前的激活输入值(就是x=WU+B,U是输入)随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,之所以训练收敛慢,一般是整体分布逐渐往非线性函数的取值区间的上下限两端靠近(对于Sigmoid函数来说,意味着激活输入值WU+B是大的负值或正值),这就导致了反向传播时低层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因。
事实上,神经网络学习过程本质上是为了学习数据的分布,而BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0、方差为1的标准正态分布,其实就是把越来越偏的分布强制拉回比较标准的分布,这样使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,从而让梯度变大,避免梯度消失问题产生,而且梯度变大意味着学习收敛速度快,因此通过BN能大大加快训练速度。
下面来看看BN的具体操作过程:

即以下四个步骤:
1.计算样本均值。
2.计算样本方差。
3.对样本数据进行标准化处理。
4.进行平移和缩放处理。这里引入了γ和β两个参数。通过训练可学习重构的γ和β这两个参数,让我们的网络可以学习恢复出原始网络所要学习的特征分布。
下面是BN层的训练流程:

这里的详细过程如下:
输入:待进入激活函数的变量。
输出:
1.这里的K,在卷积网络中可以看作是卷积核个数,如网络中第n层有64个卷积核,就需要计算64次。

注意:在正向传播时,会使用γ与β使得BN层输出与输入一样。

2.在反向传播时利用γ与β求得梯度从而改变训练权值(变量)。
3.通过不断迭代直到训练结束,求得关于不同层的γ与β。
4.不断遍历训练集中的图片,取出每个batch_size中的γ与β,最后统计每层BN的γ与β各自的和除以图片数量得到平均值,并对其做无偏估计直作为每一层的E[x]与Var[x]。
5.在预测的正向传播时,对测试数据求取γ与β,并使用该层的E[x]与Var[x],通过图中11:所表示的公式计算BN层输出。

注意:在预测时,BN层的输出已经被改变,因此BN层在预测中的作用体现在此处。

上面输入的是待进入激活函数的变量,在残差网络ResNet中,的确也是先经过BN层再用relu函数做非线性处理的。那么,为什么BN层一般用在线性层和卷积层的后面,而不是放在非线性单元即激活函数之后呢?
因为非线性单元的输出分布形状会在训练过程中变化,归一化无法消除他的方差偏移。相反的,全连接和卷积层的输出一般是一个对称、非稀疏的一个分布,更加类似高斯分布,对他们进行归一化会产生更加稳定的分布。
比如,我们对一个高斯分布的数据relu激活,那么小于0的直接就被抑制了,这样得到的结果很难是高斯分布了,这时候再添加一个BN层就很难达到所需的效果。
很多实验证明,BatchNorm只要用了就有效果,所以在一般情况下没有理由不用。但也有相反的情况,比如当每个batch里所有的sample都非常相似的时候,相似到mean和variance都基本为0时,最好不要用BatchNorm。此外如果batch size为1,从原理上来讲,此时用BatchNorm是没有任何意义的。

注意:通常我们在进行Transfer Learning的时候,会冻结之前的网络权重,注意这时候往往也会冻结BatchNorm中训练好的moving averages值。这些moving averages值只适用于以前的旧的数据,对新数据不一定适用。所以最好的方法是在Transfer Learning的时候不要冻结BatchNorm层,让moving averages值重新从新的数据中学习。


批归一化实现

这里还是使用pytorch进行实现。
准备(对下列通用):

1
2
import torch
import torch.nn as nn

  1. 2d或3d输入

    1
    2
    3
    4
    5
    6
    # 添加了可学习的仿射变换参数
    m = nn.BatchNorm1d(100)
    # 未添加可学习的仿射变换参数
    m = nn.BatchNorm1d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100))
    output = m(input)

    我们查看m,可以看到有如下形式:

    1
    BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=False, track_running_stats=True)

    这里解释一下涉及到的参数:
    num_features:来自期望输入的特征数,该期望输入的大小为:batch_size * num_features(* width)
    eps:为保证数值稳定性(分母不能趋近或取0),给分母加上的值,默认为1e-5。
    momentum:计算动态均值和动态方差并进行移动平均所使用的动量,默认为0.1。
    affine:一个布尔值,当设为true时,就给该层添加可学习的仿射变换参数。仿射变换将在后文做简单介绍。
    BatchNorm1d可以有两种输入输出:
    1.输入(N,C),输出(N,C)。
    2.输入(N,C,L),输出(N,C,L)。

  2. 3d或4d输入

    1
    2
    3
    4
    5
    m = nn.BatchNorm2d(100)
    #或者
    m = nn.BatchNorm2d(100, affine = False)
    input = torch.autograd.Variable(torch.randn(20, 100, 35, 45))
    output = m(input)

    BatchNorm2d也可以有两种输入输出:
    1.输入(N,C,L),输出(N,C,L)。
    2.输入(N,C,H,W),输出(N,C,H,W)。

  3. 4d或5d输入

    1
    2
    3
    m = nn.BatchNorm3d(100)
    #或者
    m = nn.BatchNorm3d(100, affine=False)

    BatchNorm3d同样支持两种输入输出:
    1.输入(N,C,H,W),输出(N,C,H,W)。
    2.输入(N,C,D,H,W),输出(N,C,D,H,W)。


仿射变换

这里我简单介绍一下仿射变换的概念,仿射变换(Affine Transformation或Affine Map)是一种二维坐标(x, y)到二维坐标(u, v)的变换,它是另外两种简单变换的叠加,一是线性变换,二是平移变换。同时,仿射变换保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点确定一个唯一的仿射变换。

补充:
共线性:若几个点变换前在一条线上,则仿射变换后仍然在一条线上。
平行性:若两条线变换前平行,则变换后仍然平行。
共线比例不变性:变换前一条线上两条线段的比例,在变换后比例不变。

在二维图像变换中,它的一般表达如下:

可以视为线性变换R和平移变换T的叠加。
另外,仿射变换可以通过一系列的原子变换的复合来实现,包括平移,缩放,翻转,旋转和剪切。因此我们可以将几种简单的变换矩阵相乘来实现仿射变换。


碰到底线咯 后面没有啦

本文标题:deep learning笔记:学习率衰减与批归一化

文章作者:高深远

发布时间:2019年10月01日 - 15:14

最后更新:2020年02月27日 - 07:46

原始链接:https://gsy00517.github.io/deep-learning20191001151454/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:使网络能够更深——ResNet简介与pytorch实现 | 高深远的博客

deep learning笔记:使网络能够更深——ResNet简介与pytorch实现

之前我用pytorch把ResNet18实现了一下,但由于上周准备国家奖学金答辩没有时间来写我实现的过程与总结。今天是祖国70周年华诞,借着这股喜庆劲,把这篇文章补上。

References

电子文献:
https://blog.csdn.net/weixin_43624538/article/details/85049699
https://blog.csdn.net/u013289254/article/details/98785869

参考文献:
[1]Deep Residual Learning for Image Recognition


简介

ResNet残差网络是由何恺明等四名微软研究院的华人提出的,当初看到论文标题下面的中国名字还是挺高兴的。文章引入部分,作者就探讨了深度神经网络的优化是否就只是叠加层数、增加深度那么简单。显然这是不可能的,增加深度带来的首要问题就是梯度爆炸、消散的问题,这是由于随着层数的增多,在网络中反向传播的梯度会随着连乘变得不稳定,从而变得特别大或者特别小。其中以梯度消散更为常见。值得注意的是,论文中还提到深度更深的网络反而出现准确率下降并不是由过拟合所引起的。
为了解决这个问题,研究者们做出了很多思考与尝试,其中的代表有relu激活函数的使用,Batch Normalization的使用等。关于这两种方法,可以参考网上的资料以及我的博文deep-learning笔记:开启深度学习热潮——AlexNetdeep-learning笔记:学习率衰减与批归一化
对于上面这个问题,ResNet作出的贡献是引入skip/identity connection。如下所示就是两个基本的残差模块。

上面这个block可表示为:$ F(X)=H(X)-x $。在这里,X为浅层输出,H(x)为深层的输出。当浅层的X代表的特征已经足够成熟,即当任何对于特征X的改变都会让loss变大时,F(X)会自动趋向于学习成为0,X则从恒等映射的路径继续传递。
这样,我们就可以在不增加计算成本的情况下使得在前向传递过程中,如果浅层的输出已经足够成熟(optimal),那么就让深层网络后面的层仅实现恒等映射的作用。
当X与F(X)通道数目不同时,作者尝试了两种identity mapping的方式。一种即对X缺失的通道直接补零从而使其能够对齐,这种方式比较简单直接,无需额外的参数;另一种则是通过使用1x1的conv来映射从而使通道也能达成一致。


论文

老规矩,这里还是先呈上我用黄色荧光高亮出我认为比较重要的要点的论文原文,这里我只有英文版
如果需要没有被我标注过的原文,可以直接搜索,这里我仅提供一次,可以点击这里下载。
不过,虽然没有pdf中文版,但其实深度学习CV方向一些比较经典的论文的英文、中文、中英对照都可以到Deep Learning Papers Translation上看到,非常方便。


自己实现

在论文中,作者提到了如下几个ResNet的版本的结构。

这里我实现的是ResNet18。
由于这不是我第一次使用pytorch进行实现,一些基本的使用操作我就不加注释了,想看注释来理解的话可以参考我之前VGG的实现。
由于残差的引入,导致ResNet的结构比较复杂,而论文中并没有非常详细的阐述,在研究官方源码之后,我对它的结构才有了完整的了解,这里我画出来以便参考。

注:此模块在2016年何大神的论文中给出了新的改进,可以参考我的博文deep-learning笔记:记首次ResNet实战

ResNet18的每一layer包括了两个这样的basic block,其中1x1的卷积核仅在X与F(X)通道数目不一致时进行操作,在我的代码中,我定义shortcut函数来对应一切通道一致、无需处理的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import torch
import torch.nn as nn
import torch.nn.functional as F

class ResNet(nn.Module):
def __init__(self):
super(ResNet, self).__init__()

self.conv1 = nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 7, stride = 2, padding = 3, bias = False)
self.max = nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)

self.bn1 = nn.BatchNorm2d(64)
self.bn2 = nn.BatchNorm2d(64)
self.bn3 = nn.BatchNorm2d(128)
self.bn4 = nn.BatchNorm2d(256)
self.bn5 = nn.BatchNorm2d(512)

self.shortcut = nn.Sequential()
self.shortcut3 = nn.Sequential(nn.Conv2d(64, 128, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(128))
self.shortcut4 = nn.Sequential(nn.Conv2d(128, 256, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(256))
self.shortcut5 = nn.Sequential(nn.Conv2d(256, 512, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(512))

self.conv2 = nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv3_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv3_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv4_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv4_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv5_1 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv5_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.avg = nn.AdaptiveAvgPool2d((1, 1))
#adaptive自适应,只给定输入和输出大小,让机器自行调整选择核尺寸和步长大小

self.fc = nn.Linear(512, 1000)

def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x1 = self.max(x)

#layer1
x = F.relu(self.bn2(self.conv2(x1)))
x = self.bn2(self.conv2(x))
x += self.shortcut(x1) #pytorch0.4.0之后这里要改为x = x + self.shortcut(x1)
x2 = F.relu(x)

x = F.relu(self.bn2(self.conv2(x2)))
x = self.bn2(self.conv2(x))
x += self.shortcut(x2)
x3 = F.relu(x)

#layer2
x = F.relu(self.bn3(self.conv3_1(x3)))
x = self.bn3(self.conv3_2(x))
x += self.shortcut3(x3)
x4 = F.relu(x)

x = F.relu(self.bn3(self.conv3_2(x4)))
x = self.bn3(self.conv3_2(x))
x += self.shortcut(x4)
x5 = F.relu(x)

#layer3
x = F.relu(self.bn4(self.conv4_1(x5)))
x = self.bn4(self.conv4_2(x))
x += self.shortcut4(x5)
x6 = F.relu(x)

x = F.relu(self.bn4(self.conv4_2(x6)))
x = self.bn4(self.conv4_2(x))
x += self.shortcut(x6)
x7 = F.relu(x)

#layer4
x = F.relu(self.bn5(self.conv5_1(x7)))
x = self.bn5(self.conv5_2(x))
x += self.shortcut5(x7)
x8 = F.relu(x)

x = F.relu(self.bn5(self.conv5_2(x8)))
x = self.bn5(self.conv5_2(x))
x += self.shortcut(x8)
x = F.relu(x)

#ending
x = self.avg(x)

#变换维度,可以设其中一个尺寸为-1,表示机器内部自己计算,但同时只能有一个为-1
x = x.view(-1, self.num_flat_features(x))
x = self.fc(x)

x = F.softmax(x, dim = 1)

return x

def num_flat_features(self, x):
size = x.size()[1:]
num_features = 1
for s in size:
num_features *= s
return num_features

net = ResNet()

同样的,我们可以随机生成一个张量来进行验证:

1
2
3
input = torch.randn(1, 3, 48, 48)
out = net(input)
print(out)

如果可以顺利地输出,那么模型基本上是没有问题的。


出现的问题

在这里我还是想把自己踩的一些简单的坑记下来,引以为戒。

  1. softmax输出全为1

    当我使用F.softmax之后,出现了这样的一个问题:

    查找资料后发现,我错误的把对每一行softmax当作了对每一列softmax。因为这个softmax语句是我从之前的自己做的一道kaggle题目写的代码中ctrl+C+V过来的,复制过来的是x = F.softmax(x, dim = 0),在这里,dim = 0意味着我对张量的每一列进行softmax,这是因为我之前的场景中需要处理的张量是一维的,也就是tensor()里面只有一对“[]”,此时它默认只有一列,我对列进行softmax自然就没有问题。
    而放到这里,我再对列进行softmax时,每列上就只有一个元素。那么结果就都是1即100%了。解决的方法就是把dim设为1。
    下面我在用一组代码直观地展示一下softmax的用法与区别。

    1
    2
    3
    4
    5
    6
    7
    import torch
    import torch.nn.functional as F
    x1= torch.Tensor( [ [1, 2, 3, 4], [1, 3, 4, 5], [3, 4, 5, 6]])
    y11= F.softmax(x1, dim = 0) #对每一列进行softmax
    y12 = F.softmax(x1, dim = 1) #对每一行进行softmax
    x2 = torch.Tensor([1, 2, 3, 4])
    y2 = F.softmax(x2, dim = 0)

    我们输出每个结果,可以看到:

  2. bias

    或许你可以发现,在我的代码中,每个卷积层中都设置了bias = False,这是我在参考官方源码之后补上的。那么,这个bias是什么,又有什么用呢?
    我们在学深度学习的时候,最早接触到的神经网络应该是感知器,它的结构如下图所示。 要想激活这个感知器,就必须使x1 * w1 + x2 * w2 + ... + xn * wn > T(T为一个阈值),而T越大,想激活这个感知器的难度越大。
    考虑样本较多的情况,我不可能手动选择一个阈值,使得模型整体表现最佳,因此我们不如使得T变成可学习的,这样一来,T会自动学习到一个数,使得模型的整体表现最佳。当把T移动到左边,它就成了bias偏置,x1 * w1 + x2 * w2 + ... + xn * wn - T > 0。显然,偏置的大小控制着激活这个感知器的难易程度。
    在比感知器高级的神经网络中,也是如此。
    但倘若我们要在卷积后面加上归一化操作,那么bias的作用就无法体现了。
    我们以ResNet卷积层后的BN层为例。
    可参考我的上一篇博文,BN处理过程中有这样一步: 对于分子而言,无论有没有bias,对结果都没有影响;而对于下面分母而言,因为是方差操作,所以也没有影响。因此,在ResNet中,因为每次卷积之后都要进行BN操作,那就不需要启用bias,否则非但不起作用,还会消耗一定的显卡内存。

官方源码

如果你此时对ResNet的结构已经有了比较清晰的理解,那么可以尝试着来理解一下官方源码的思路。其实我觉得先看像我这样直观的代码实现再看官方源码更有助理解且更高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import torch
import torch.nn as nn
from .utils import load_state_dict_from_url


__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',
'resnet152', 'resnext50_32x4d', 'resnext101_32x8d',
'wide_resnet50_2', 'wide_resnet101_2']


model_urls = {
'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
'resnext50_32x4d': 'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth',
'resnext101_32x8d': 'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth',
'wide_resnet50_2': 'https://download.pytorch.org/models/wide_resnet50_2-95faca4d.pth',
'wide_resnet101_2': 'https://download.pytorch.org/models/wide_resnet101_2-32ee1156.pth',
}


def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)


def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)


class BasicBlock(nn.Module):
expansion = 1
__constants__ = ['downsample']

def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride

def forward(self, x):
identity = x

out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)

out = self.conv2(out)
out = self.bn2(out)

if self.downsample is not None:
identity = self.downsample(x)

out += identity
out = self.relu(out)

return out


class Bottleneck(nn.Module):
expansion = 4

def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride

def forward(self, x):
identity = x

out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)

out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)

out = self.conv3(out)
out = self.bn3(out)

if self.downsample is not None:
identity = self.downsample(x)

out += identity
out = self.relu(out)

return out


class ResNet(nn.Module):

def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer

self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2,
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)

for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)

# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)

def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)

layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))

return nn.Sequential(*layers)

def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)

x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)

x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)

return x


def _resnet(arch, block, layers, pretrained, progress, **kwargs):
model = ResNet(block, layers, **kwargs)
if pretrained:
state_dict = load_state_dict_from_url(model_urls[arch],
progress=progress)
model.load_state_dict(state_dict)
return model


def resnet18(pretrained=False, progress=True, **kwargs):
r"""ResNet-18 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress,
**kwargs)


def resnet34(pretrained=False, progress=True, **kwargs):
r"""ResNet-34 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress,
**kwargs)


def resnet50(pretrained=False, progress=True, **kwargs):
r"""ResNet-50 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress,
**kwargs)


def resnet101(pretrained=False, progress=True, **kwargs):
r"""ResNet-101 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained, progress,
**kwargs)


def resnet152(pretrained=False, progress=True, **kwargs):
r"""ResNet-152 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained, progress,
**kwargs)


def resnext50_32x4d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-50 32x4d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 4
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)


def resnext101_32x8d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-101 32x8d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 8
return _resnet('resnext101_32x8d', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)


def wide_resnet50_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-50-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_

The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet50_2', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)


def wide_resnet101_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-101-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_

The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet101_2', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)


pth文件

在阅读官方源码时,我们会注意到官方提供了一系列版本的model_urls,其中,每一个url都是以.pth结尾的。
当我下载了对应的文件之后,并不知道如何处理,于是我通过搜索,简单的了解了pth文件的概念与使用方法。
简单来说,pth文件就是一个表示Python的模块搜索路径(module search path)的文本文件,在xxx.pth文件里面,会书写一些路径,一行一个。如果我们将xxx.pth文件放在特定位置,则可以让python在加载模块时,读取xxx.pth中指定的路径。
下面我使用pytorch对pth文件进行加载操作。
首先,我把ResNet18对应的pth文件下载到桌面。

1
2
3
4
5
6
7
8
9
10
11
import torch

import torchvision.models as models

# pretrained = True就可以使用预训练的模型
net = models.resnet18(pretrained = False)
#注意,根据model的不同,这里models.xxx的内容也是不同的,比如models.squeezenet1_1

pthfile = r'C:\Users\sheny\Desktop\resnet18-5c106cde.pth'#pth文件所在路径
net.load_state_dict(torch.load(pthfile))
print(net)

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)

这样你就可以看到很详尽的参数设置了。
我们还可以加载所有的参数。

1
2
3
4
5
6
7
import torch

pthfile = r'C:\Users\sheny\Desktop\resnet18-5c106cde.pth'

net = torch.load(pthfile)

print(net)

输出如下:

1
2
3
4
5
6
7
8
OrderedDict([('conv1.weight', Parameter containing:
tensor([[[[-1.0419e-02, -6.1356e-03, -1.8098e-03, ..., 5.6615e-02,
1.7083e-02, -1.2694e-02],
[ 1.1083e-02, 9.5276e-03, -1.0993e-01, ..., -2.7124e-01,
-1.2907e-01, 3.7424e-03],
[-6.9434e-03, 5.9089e-02, 2.9548e-01, ..., 5.1972e-01,
2.5632e-01, 6.3573e-02],
...,


碰到底线咯 后面没有啦

本文标题:deep learning笔记:使网络能够更深——ResNet简介与pytorch实现

文章作者:高深远

发布时间:2019年10月01日 - 18:42

最后更新:2020年02月15日 - 22:09

原始链接:https://gsy00517.github.io/deep-learning20191001184216/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:使网络能够更深——ResNet简介与pytorch实现 | 高深远的博客

deep learning笔记:使网络能够更深——ResNet简介与pytorch实现

之前我用pytorch把ResNet18实现了一下,但由于上周准备国家奖学金答辩没有时间来写我实现的过程与总结。今天是祖国70周年华诞,借着这股喜庆劲,把这篇文章补上。

References

电子文献:
https://blog.csdn.net/weixin_43624538/article/details/85049699
https://blog.csdn.net/u013289254/article/details/98785869

参考文献:
[1]Deep Residual Learning for Image Recognition


简介

ResNet残差网络是由何恺明等四名微软研究院的华人提出的,当初看到论文标题下面的中国名字还是挺高兴的。文章引入部分,作者就探讨了深度神经网络的优化是否就只是叠加层数、增加深度那么简单。显然这是不可能的,增加深度带来的首要问题就是梯度爆炸、消散的问题,这是由于随着层数的增多,在网络中反向传播的梯度会随着连乘变得不稳定,从而变得特别大或者特别小。其中以梯度消散更为常见。值得注意的是,论文中还提到深度更深的网络反而出现准确率下降并不是由过拟合所引起的。
为了解决这个问题,研究者们做出了很多思考与尝试,其中的代表有relu激活函数的使用,Batch Normalization的使用等。关于这两种方法,可以参考网上的资料以及我的博文deep-learning笔记:开启深度学习热潮——AlexNetdeep-learning笔记:学习率衰减与批归一化
对于上面这个问题,ResNet作出的贡献是引入skip/identity connection。如下所示就是两个基本的残差模块。

上面这个block可表示为:$ F(X)=H(X)-x $。在这里,X为浅层输出,H(x)为深层的输出。当浅层的X代表的特征已经足够成熟,即当任何对于特征X的改变都会让loss变大时,F(X)会自动趋向于学习成为0,X则从恒等映射的路径继续传递。
这样,我们就可以在不增加计算成本的情况下使得在前向传递过程中,如果浅层的输出已经足够成熟(optimal),那么就让深层网络后面的层仅实现恒等映射的作用。
当X与F(X)通道数目不同时,作者尝试了两种identity mapping的方式。一种即对X缺失的通道直接补零从而使其能够对齐,这种方式比较简单直接,无需额外的参数;另一种则是通过使用1x1的conv来映射从而使通道也能达成一致。


论文

老规矩,这里还是先呈上我用黄色荧光高亮出我认为比较重要的要点的论文原文,这里我只有英文版
如果需要没有被我标注过的原文,可以直接搜索,这里我仅提供一次,可以点击这里下载。
不过,虽然没有pdf中文版,但其实深度学习CV方向一些比较经典的论文的英文、中文、中英对照都可以到Deep Learning Papers Translation上看到,非常方便。


自己实现

在论文中,作者提到了如下几个ResNet的版本的结构。

这里我实现的是ResNet18。
由于这不是我第一次使用pytorch进行实现,一些基本的使用操作我就不加注释了,想看注释来理解的话可以参考我之前VGG的实现。
由于残差的引入,导致ResNet的结构比较复杂,而论文中并没有非常详细的阐述,在研究官方源码之后,我对它的结构才有了完整的了解,这里我画出来以便参考。

注:此模块在2016年何大神的论文中给出了新的改进,可以参考我的博文deep-learning笔记:记首次ResNet实战

ResNet18的每一layer包括了两个这样的basic block,其中1x1的卷积核仅在X与F(X)通道数目不一致时进行操作,在我的代码中,我定义shortcut函数来对应一切通道一致、无需处理的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import torch
import torch.nn as nn
import torch.nn.functional as F

class ResNet(nn.Module):
def __init__(self):
super(ResNet, self).__init__()

self.conv1 = nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 7, stride = 2, padding = 3, bias = False)
self.max = nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)

self.bn1 = nn.BatchNorm2d(64)
self.bn2 = nn.BatchNorm2d(64)
self.bn3 = nn.BatchNorm2d(128)
self.bn4 = nn.BatchNorm2d(256)
self.bn5 = nn.BatchNorm2d(512)

self.shortcut = nn.Sequential()
self.shortcut3 = nn.Sequential(nn.Conv2d(64, 128, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(128))
self.shortcut4 = nn.Sequential(nn.Conv2d(128, 256, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(256))
self.shortcut5 = nn.Sequential(nn.Conv2d(256, 512, kernel_size = 1, stride = 2, bias = False), nn.BatchNorm2d(512))

self.conv2 = nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv3_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv3_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv4_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv4_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.conv5_1 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, stride = 2, padding = 1, bias = False)
self.conv5_2 = nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, stride = 1, padding = 1, bias = False)

self.avg = nn.AdaptiveAvgPool2d((1, 1))
#adaptive自适应,只给定输入和输出大小,让机器自行调整选择核尺寸和步长大小

self.fc = nn.Linear(512, 1000)

def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x1 = self.max(x)

#layer1
x = F.relu(self.bn2(self.conv2(x1)))
x = self.bn2(self.conv2(x))
x += self.shortcut(x1) #pytorch0.4.0之后这里要改为x = x + self.shortcut(x1)
x2 = F.relu(x)

x = F.relu(self.bn2(self.conv2(x2)))
x = self.bn2(self.conv2(x))
x += self.shortcut(x2)
x3 = F.relu(x)

#layer2
x = F.relu(self.bn3(self.conv3_1(x3)))
x = self.bn3(self.conv3_2(x))
x += self.shortcut3(x3)
x4 = F.relu(x)

x = F.relu(self.bn3(self.conv3_2(x4)))
x = self.bn3(self.conv3_2(x))
x += self.shortcut(x4)
x5 = F.relu(x)

#layer3
x = F.relu(self.bn4(self.conv4_1(x5)))
x = self.bn4(self.conv4_2(x))
x += self.shortcut4(x5)
x6 = F.relu(x)

x = F.relu(self.bn4(self.conv4_2(x6)))
x = self.bn4(self.conv4_2(x))
x += self.shortcut(x6)
x7 = F.relu(x)

#layer4
x = F.relu(self.bn5(self.conv5_1(x7)))
x = self.bn5(self.conv5_2(x))
x += self.shortcut5(x7)
x8 = F.relu(x)

x = F.relu(self.bn5(self.conv5_2(x8)))
x = self.bn5(self.conv5_2(x))
x += self.shortcut(x8)
x = F.relu(x)

#ending
x = self.avg(x)

#变换维度,可以设其中一个尺寸为-1,表示机器内部自己计算,但同时只能有一个为-1
x = x.view(-1, self.num_flat_features(x))
x = self.fc(x)

x = F.softmax(x, dim = 1)

return x

def num_flat_features(self, x):
size = x.size()[1:]
num_features = 1
for s in size:
num_features *= s
return num_features

net = ResNet()

同样的,我们可以随机生成一个张量来进行验证:

1
2
3
input = torch.randn(1, 3, 48, 48)
out = net(input)
print(out)

如果可以顺利地输出,那么模型基本上是没有问题的。


出现的问题

在这里我还是想把自己踩的一些简单的坑记下来,引以为戒。

  1. softmax输出全为1

    当我使用F.softmax之后,出现了这样的一个问题:

    查找资料后发现,我错误的把对每一行softmax当作了对每一列softmax。因为这个softmax语句是我从之前的自己做的一道kaggle题目写的代码中ctrl+C+V过来的,复制过来的是x = F.softmax(x, dim = 0),在这里,dim = 0意味着我对张量的每一列进行softmax,这是因为我之前的场景中需要处理的张量是一维的,也就是tensor()里面只有一对“[]”,此时它默认只有一列,我对列进行softmax自然就没有问题。
    而放到这里,我再对列进行softmax时,每列上就只有一个元素。那么结果就都是1即100%了。解决的方法就是把dim设为1。
    下面我在用一组代码直观地展示一下softmax的用法与区别。

    1
    2
    3
    4
    5
    6
    7
    import torch
    import torch.nn.functional as F
    x1= torch.Tensor( [ [1, 2, 3, 4], [1, 3, 4, 5], [3, 4, 5, 6]])
    y11= F.softmax(x1, dim = 0) #对每一列进行softmax
    y12 = F.softmax(x1, dim = 1) #对每一行进行softmax
    x2 = torch.Tensor([1, 2, 3, 4])
    y2 = F.softmax(x2, dim = 0)

    我们输出每个结果,可以看到:

  2. bias

    或许你可以发现,在我的代码中,每个卷积层中都设置了bias = False,这是我在参考官方源码之后补上的。那么,这个bias是什么,又有什么用呢?
    我们在学深度学习的时候,最早接触到的神经网络应该是感知器,它的结构如下图所示。 要想激活这个感知器,就必须使x1 * w1 + x2 * w2 + ... + xn * wn > T(T为一个阈值),而T越大,想激活这个感知器的难度越大。
    考虑样本较多的情况,我不可能手动选择一个阈值,使得模型整体表现最佳,因此我们不如使得T变成可学习的,这样一来,T会自动学习到一个数,使得模型的整体表现最佳。当把T移动到左边,它就成了bias偏置,x1 * w1 + x2 * w2 + ... + xn * wn - T > 0。显然,偏置的大小控制着激活这个感知器的难易程度。
    在比感知器高级的神经网络中,也是如此。
    但倘若我们要在卷积后面加上归一化操作,那么bias的作用就无法体现了。
    我们以ResNet卷积层后的BN层为例。
    可参考我的上一篇博文,BN处理过程中有这样一步: 对于分子而言,无论有没有bias,对结果都没有影响;而对于下面分母而言,因为是方差操作,所以也没有影响。因此,在ResNet中,因为每次卷积之后都要进行BN操作,那就不需要启用bias,否则非但不起作用,还会消耗一定的显卡内存。

官方源码

如果你此时对ResNet的结构已经有了比较清晰的理解,那么可以尝试着来理解一下官方源码的思路。其实我觉得先看像我这样直观的代码实现再看官方源码更有助理解且更高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import torch
import torch.nn as nn
from .utils import load_state_dict_from_url


__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',
'resnet152', 'resnext50_32x4d', 'resnext101_32x8d',
'wide_resnet50_2', 'wide_resnet101_2']


model_urls = {
'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',
'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',
'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
'resnext50_32x4d': 'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth',
'resnext101_32x8d': 'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth',
'wide_resnet50_2': 'https://download.pytorch.org/models/wide_resnet50_2-95faca4d.pth',
'wide_resnet101_2': 'https://download.pytorch.org/models/wide_resnet101_2-32ee1156.pth',
}


def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)


def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)


class BasicBlock(nn.Module):
expansion = 1
__constants__ = ['downsample']

def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride

def forward(self, x):
identity = x

out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)

out = self.conv2(out)
out = self.bn2(out)

if self.downsample is not None:
identity = self.downsample(x)

out += identity
out = self.relu(out)

return out


class Bottleneck(nn.Module):
expansion = 4

def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride

def forward(self, x):
identity = x

out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)

out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)

out = self.conv3(out)
out = self.bn3(out)

if self.downsample is not None:
identity = self.downsample(x)

out += identity
out = self.relu(out)

return out


class ResNet(nn.Module):

def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer

self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
# each element in the tuple indicates if we should replace
# the 2x2 stride with a dilated convolution instead
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2,
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)

for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)

# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)

def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)

layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))

return nn.Sequential(*layers)

def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)

x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)

x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)

return x


def _resnet(arch, block, layers, pretrained, progress, **kwargs):
model = ResNet(block, layers, **kwargs)
if pretrained:
state_dict = load_state_dict_from_url(model_urls[arch],
progress=progress)
model.load_state_dict(state_dict)
return model


def resnet18(pretrained=False, progress=True, **kwargs):
r"""ResNet-18 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress,
**kwargs)


def resnet34(pretrained=False, progress=True, **kwargs):
r"""ResNet-34 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet34', BasicBlock, [3, 4, 6, 3], pretrained, progress,
**kwargs)


def resnet50(pretrained=False, progress=True, **kwargs):
r"""ResNet-50 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress,
**kwargs)


def resnet101(pretrained=False, progress=True, **kwargs):
r"""ResNet-101 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet101', Bottleneck, [3, 4, 23, 3], pretrained, progress,
**kwargs)


def resnet152(pretrained=False, progress=True, **kwargs):
r"""ResNet-152 model from
`"Deep Residual Learning for Image Recognition" <https://arxiv.org/pdf/1512.03385.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _resnet('resnet152', Bottleneck, [3, 8, 36, 3], pretrained, progress,
**kwargs)


def resnext50_32x4d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-50 32x4d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 4
return _resnet('resnext50_32x4d', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)


def resnext101_32x8d(pretrained=False, progress=True, **kwargs):
r"""ResNeXt-101 32x8d model from
`"Aggregated Residual Transformation for Deep Neural Networks" <https://arxiv.org/pdf/1611.05431.pdf>`_

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['groups'] = 32
kwargs['width_per_group'] = 8
return _resnet('resnext101_32x8d', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)


def wide_resnet50_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-50-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_

The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet50_2', Bottleneck, [3, 4, 6, 3],
pretrained, progress, **kwargs)


def wide_resnet101_2(pretrained=False, progress=True, **kwargs):
r"""Wide ResNet-101-2 model from
`"Wide Residual Networks" <https://arxiv.org/pdf/1605.07146.pdf>`_

The model is the same as ResNet except for the bottleneck number of channels
which is twice larger in every block. The number of channels in outer 1x1
convolutions is the same, e.g. last block in ResNet-50 has 2048-512-2048
channels, and in Wide ResNet-50-2 has 2048-1024-2048.

Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
kwargs['width_per_group'] = 64 * 2
return _resnet('wide_resnet101_2', Bottleneck, [3, 4, 23, 3],
pretrained, progress, **kwargs)


pth文件

在阅读官方源码时,我们会注意到官方提供了一系列版本的model_urls,其中,每一个url都是以.pth结尾的。
当我下载了对应的文件之后,并不知道如何处理,于是我通过搜索,简单的了解了pth文件的概念与使用方法。
简单来说,pth文件就是一个表示Python的模块搜索路径(module search path)的文本文件,在xxx.pth文件里面,会书写一些路径,一行一个。如果我们将xxx.pth文件放在特定位置,则可以让python在加载模块时,读取xxx.pth中指定的路径。
下面我使用pytorch对pth文件进行加载操作。
首先,我把ResNet18对应的pth文件下载到桌面。

1
2
3
4
5
6
7
8
9
10
11
import torch

import torchvision.models as models

# pretrained = True就可以使用预训练的模型
net = models.resnet18(pretrained = False)
#注意,根据model的不同,这里models.xxx的内容也是不同的,比如models.squeezenet1_1

pthfile = r'C:\Users\sheny\Desktop\resnet18-5c106cde.pth'#pth文件所在路径
net.load_state_dict(torch.load(pthfile))
print(net)

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=512, out_features=1000, bias=True)
)

这样你就可以看到很详尽的参数设置了。
我们还可以加载所有的参数。

1
2
3
4
5
6
7
import torch

pthfile = r'C:\Users\sheny\Desktop\resnet18-5c106cde.pth'

net = torch.load(pthfile)

print(net)

输出如下:

1
2
3
4
5
6
7
8
OrderedDict([('conv1.weight', Parameter containing:
tensor([[[[-1.0419e-02, -6.1356e-03, -1.8098e-03, ..., 5.6615e-02,
1.7083e-02, -1.2694e-02],
[ 1.1083e-02, 9.5276e-03, -1.0993e-01, ..., -2.7124e-01,
-1.2907e-01, 3.7424e-03],
[-6.9434e-03, 5.9089e-02, 2.9548e-01, ..., 5.1972e-01,
2.5632e-01, 6.3573e-02],
...,


碰到底线咯 后面没有啦

本文标题:deep learning笔记:使网络能够更深——ResNet简介与pytorch实现

文章作者:高深远

发布时间:2019年10月01日 - 18:42

最后更新:2020年02月15日 - 22:09

原始链接:https://gsy00517.github.io/deep-learning20191001184216/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:记首次ResNet实战 | 高深远的博客

deep learning笔记:记首次ResNet实战

实践出真知。在之前的博文deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现中,我对何恺明大神的CVPR最佳论文中提出的残差网络做了简单介绍。而就在第二年(2016年),何恺明的团队就发表了“Identity Mappings in Deep Residual Networks”这篇文章,分析了ResNet成功的关键因素——residual block背后的算法,并对residual block以及after-addition activation进行改进,通过一系列的ablation experiments验证了,在residual block和after-addition activation上都使用identity mapping(恒等映射)时,能对模型训练产生很好的效果。不知道为什么,我今天从arXiv上download这篇paper的时候发现上不去了,莫非现在上arXiv也要科学上网了?
本次实战主要是基于之前的ResNet实现和flyAI平台,并结合上面提到的何恺明团队分析ResNet的论文做出一些改进,并检验效果。

References

电子文献:
https://blog.csdn.net/Sandwichsauce/article/details/89162570
https://www.jianshu.com/p/184799230f20
https://blog.csdn.net/wspba/article/details/60572886
https://www.cnblogs.com/4991tcl/p/10395574.html
https://blog.csdn.net/DuinoDu/article/details/80435127

参考文献:
[1]Identity Mappings in Deep Residual Networks


ablation experiments

在上面我提到了这个名词,中文翻译是“消融实验”。或许在阅读论文的过程中会接触到这个名词,如果仅根据字面翻译的话或许会很纳闷。
在查找了一定的资料后,我对这种方法有了大致地了解。
ablation的原本释义是通过机械方法切除身体组织,如手术,从身体中去除尤指器官以及异常生长的有害物质。
事实上,这种方法类似于物理实验中的控制变量法,即当在一个新提出的模型中同时改变了多个条件或者参数,那么为了分析和检验,在接下去的消融实验中,会一一控制每个条件或者参数不变,来根据结果分析到底是哪个条件或者参数对模型的优化、影响更大。
在机器学习、特别是复杂的深度神经网络的背景下,科研工作者们已经采用“消融研究”来描述去除网络的某些部分的过程,以便更好地理解网络的行为。


ResNet的分析与改进

  1. 残差单元

    在2015年ResNet首次发布的时候,设计的残差单元在最后的输出之前是要经过一个激活函数的。而在2016年新提出的残差单元中,去掉了这个激活函数,并通过实验证明新提出的残差单元训练更简单。 这种新的构造的关键在于不仅仅是在残差单元的内部,而是在整个网络中创建一个“直接”的计算传播路径来分析深度残差网络。通过构造这样一个“干净”的信息通路,可以在前向和反向传播阶段,使信号能够直接的从一个单元传递到其他任意一个单元。实验表明,当框架接近于上面的状态时,训练会变得更加简单。
  2. shortcut

    对于恒等跳跃连接$h(x_{l})=x_{l}$,作者设计了5种新的连接方式来与原本的方式作对比,设计以及实验结果如下所示: 其中fail表示测试误差超过了20%。实验结果表明,原本的连接方式误差衰减最快,同时误差也最低,而其他形式的shortcut都产生了较大的损失和误差。
    作者认为,shortcut连接中的操作 (缩放、门控、1×1的卷积以及dropout) 会阻碍信息的传递,以致于对优化造成困难。
    此外,虽然1×1的卷积shortcut连接引入了更多的参数,本应该比恒等shortcut连接具有更加强大的表达能力。但是它的效果并不好,这表明了这些模型退化问题的原因是优化问题,而不是表达能力的问题。
  3. 激活函数

    对于激活函数的设置,作者设计了如下几种方式进行比较: 在这里,作者将激活项分为了预激活(pre-activation)和后激活(post-activation)。通过实验可以发现,将ReLU和BN都放在预激活中,即full pre-activation最为有效。

ResNet实战

根据论文中的实验结果,我使用了新的残差模块进行实践。并结合在deep-learning笔记:学习率衰减与批归一化中的分析总结对BN层的位置选取作了简单调整。在本次实验中,我尝试使用了StepLR阶梯式衰减和连续衰减两种学习率衰减方式,事实证明,使用StepLR阶梯式衰减的效果在这里要略好一些(连续衰减前期学得太快,后面大半部分都学不动了…)。
首次训练的结果并不理想,于是我加大了学习率每次衰减的幅度,即让最后阶段的学习率更小,这使我的模型的评分提高了不少。
由于训练资源有限,我没能进行更深(仅设置了10层左右)、更久(每次仅进行20个epoch)的训练,但在每个batch中,最高的accuracy也能达到65%左右,平均大约能超过50%。相比之前使用浅层网络仅能达到20%左右的accuracy,这已经提升不少了。然而最终的打分还是没有显著提高,因此我思考是否存在过拟合的问题。为此我尝试着在全连接层和捷径连接中加入dropout正则化来提高在测试集中的泛化能力,结果最终打分仅提高了0.1,而训练时间稍短。由于我除了dropout之外并没有改变网络的层数等影响参数量的因素,因此似乎与何大神在论文中original版和dropout版shortchut的比较有一些矛盾,但的确还是说明了dropout在这里的作用微乎其微,优化模型时可以排除至考虑范围之外了。


遇到的问题

  1. TabError: inconsistent use of tabs and spaces in indentation

    当我在flyAI提供的窗口中修改代码并提交GPU训练时,就出现了这个报错。它说我在缩进时错误的使用了制表符和空格。于是我只好把报错处的缩进删除并重敲tab缩进,问题就得到了解决。
    如果使用PyCharm等IDE的话,这个错误会直接显示出来,即在缩进处会有灰色的颜色警告,将光标移过去就会有具体报错。这就省得提交GPU之后才能收到报错,所以以后写代码、改代码能用IDE还是用起来好啦。
  2. RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation

    这是在shortcut残差连接时遇到的一个报错,上网后发现原因很简单:版本问题。在新版的pytorch中,由于0.4.0之后把Varible和Tensor融合为一个Tensor,因此inplace操作在之前对Varible时还能用,但现在只有Tensor,就会出错了。
    解决的办法是将x += self.shortcut(x1)替换成x = x + self.shortcut(x1)
    若网络很大,找起来很麻烦,可以在网络的中间变量加一句x.backward(),看会不会报错,如果不会的话,那就说明至少这之前是没毛病的。
  3. 张量第一维是batch size

    起初,我根据输入的torch.Size([64, 1, 128, 128]),使用如下函数将输出拍平成1维的:

    1
    2
    3
    4
    5
    6
    def num_flat_features(self, x):
    size = x.size()[0:]
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    同时,为了匹配,我将第一个全连接层的输入乘上了64。其实这个时候我已经开始怀疑这个64是哪来的了,为什么这个张量第一维尺度有64。
    直到后来平台报错,我才意识到这个表示的不是数据的维度,而是我设计的batch size。
    为此我将上面的代码调整如下:

    1
    2
    3
    4
    5
    6
    def num_flat_features(self, x):
    size = x.size()[1:]
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    如此,问题得到解决,最终的输出应该是batch size乘上总类别数的一个张量。


arXiv

文前提到了上arXiv下论文要科学上网的事情,后来我发现了一个中科院理论物理所的一个备选镜像,但是好像不是特别稳定,不过还是先留在这里吧,万一的话可以拿来试试。
一般一些科研工作者会在论文发布之前上传到arXiv以防止自己的idea被别人用了。估计主要是为了防止类似牛顿莱布尼兹之争这种事吧。


碰到底线咯 后面没有啦

本文标题:deep learning笔记:记首次ResNet实战

文章作者:高深远

发布时间:2020年01月13日 - 17:47

最后更新:2020年02月15日 - 22:10

原始链接:https://gsy00517.github.io/deep-learning20200113174731/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:记首次ResNet实战 | 高深远的博客

deep learning笔记:记首次ResNet实战

实践出真知。在之前的博文deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现中,我对何恺明大神的CVPR最佳论文中提出的残差网络做了简单介绍。而就在第二年(2016年),何恺明的团队就发表了“Identity Mappings in Deep Residual Networks”这篇文章,分析了ResNet成功的关键因素——residual block背后的算法,并对residual block以及after-addition activation进行改进,通过一系列的ablation experiments验证了,在residual block和after-addition activation上都使用identity mapping(恒等映射)时,能对模型训练产生很好的效果。不知道为什么,我今天从arXiv上download这篇paper的时候发现上不去了,莫非现在上arXiv也要科学上网了?
本次实战主要是基于之前的ResNet实现和flyAI平台,并结合上面提到的何恺明团队分析ResNet的论文做出一些改进,并检验效果。

References

电子文献:
https://blog.csdn.net/Sandwichsauce/article/details/89162570
https://www.jianshu.com/p/184799230f20
https://blog.csdn.net/wspba/article/details/60572886
https://www.cnblogs.com/4991tcl/p/10395574.html
https://blog.csdn.net/DuinoDu/article/details/80435127

参考文献:
[1]Identity Mappings in Deep Residual Networks


ablation experiments

在上面我提到了这个名词,中文翻译是“消融实验”。或许在阅读论文的过程中会接触到这个名词,如果仅根据字面翻译的话或许会很纳闷。
在查找了一定的资料后,我对这种方法有了大致地了解。
ablation的原本释义是通过机械方法切除身体组织,如手术,从身体中去除尤指器官以及异常生长的有害物质。
事实上,这种方法类似于物理实验中的控制变量法,即当在一个新提出的模型中同时改变了多个条件或者参数,那么为了分析和检验,在接下去的消融实验中,会一一控制每个条件或者参数不变,来根据结果分析到底是哪个条件或者参数对模型的优化、影响更大。
在机器学习、特别是复杂的深度神经网络的背景下,科研工作者们已经采用“消融研究”来描述去除网络的某些部分的过程,以便更好地理解网络的行为。


ResNet的分析与改进

  1. 残差单元

    在2015年ResNet首次发布的时候,设计的残差单元在最后的输出之前是要经过一个激活函数的。而在2016年新提出的残差单元中,去掉了这个激活函数,并通过实验证明新提出的残差单元训练更简单。 这种新的构造的关键在于不仅仅是在残差单元的内部,而是在整个网络中创建一个“直接”的计算传播路径来分析深度残差网络。通过构造这样一个“干净”的信息通路,可以在前向和反向传播阶段,使信号能够直接的从一个单元传递到其他任意一个单元。实验表明,当框架接近于上面的状态时,训练会变得更加简单。
  2. shortcut

    对于恒等跳跃连接$h(x_{l})=x_{l}$,作者设计了5种新的连接方式来与原本的方式作对比,设计以及实验结果如下所示: 其中fail表示测试误差超过了20%。实验结果表明,原本的连接方式误差衰减最快,同时误差也最低,而其他形式的shortcut都产生了较大的损失和误差。
    作者认为,shortcut连接中的操作 (缩放、门控、1×1的卷积以及dropout) 会阻碍信息的传递,以致于对优化造成困难。
    此外,虽然1×1的卷积shortcut连接引入了更多的参数,本应该比恒等shortcut连接具有更加强大的表达能力。但是它的效果并不好,这表明了这些模型退化问题的原因是优化问题,而不是表达能力的问题。
  3. 激活函数

    对于激活函数的设置,作者设计了如下几种方式进行比较: 在这里,作者将激活项分为了预激活(pre-activation)和后激活(post-activation)。通过实验可以发现,将ReLU和BN都放在预激活中,即full pre-activation最为有效。

ResNet实战

根据论文中的实验结果,我使用了新的残差模块进行实践。并结合在deep-learning笔记:学习率衰减与批归一化中的分析总结对BN层的位置选取作了简单调整。在本次实验中,我尝试使用了StepLR阶梯式衰减和连续衰减两种学习率衰减方式,事实证明,使用StepLR阶梯式衰减的效果在这里要略好一些(连续衰减前期学得太快,后面大半部分都学不动了…)。
首次训练的结果并不理想,于是我加大了学习率每次衰减的幅度,即让最后阶段的学习率更小,这使我的模型的评分提高了不少。
由于训练资源有限,我没能进行更深(仅设置了10层左右)、更久(每次仅进行20个epoch)的训练,但在每个batch中,最高的accuracy也能达到65%左右,平均大约能超过50%。相比之前使用浅层网络仅能达到20%左右的accuracy,这已经提升不少了。然而最终的打分还是没有显著提高,因此我思考是否存在过拟合的问题。为此我尝试着在全连接层和捷径连接中加入dropout正则化来提高在测试集中的泛化能力,结果最终打分仅提高了0.1,而训练时间稍短。由于我除了dropout之外并没有改变网络的层数等影响参数量的因素,因此似乎与何大神在论文中original版和dropout版shortchut的比较有一些矛盾,但的确还是说明了dropout在这里的作用微乎其微,优化模型时可以排除至考虑范围之外了。


遇到的问题

  1. TabError: inconsistent use of tabs and spaces in indentation

    当我在flyAI提供的窗口中修改代码并提交GPU训练时,就出现了这个报错。它说我在缩进时错误的使用了制表符和空格。于是我只好把报错处的缩进删除并重敲tab缩进,问题就得到了解决。
    如果使用PyCharm等IDE的话,这个错误会直接显示出来,即在缩进处会有灰色的颜色警告,将光标移过去就会有具体报错。这就省得提交GPU之后才能收到报错,所以以后写代码、改代码能用IDE还是用起来好啦。
  2. RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation

    这是在shortcut残差连接时遇到的一个报错,上网后发现原因很简单:版本问题。在新版的pytorch中,由于0.4.0之后把Varible和Tensor融合为一个Tensor,因此inplace操作在之前对Varible时还能用,但现在只有Tensor,就会出错了。
    解决的办法是将x += self.shortcut(x1)替换成x = x + self.shortcut(x1)
    若网络很大,找起来很麻烦,可以在网络的中间变量加一句x.backward(),看会不会报错,如果不会的话,那就说明至少这之前是没毛病的。
  3. 张量第一维是batch size

    起初,我根据输入的torch.Size([64, 1, 128, 128]),使用如下函数将输出拍平成1维的:

    1
    2
    3
    4
    5
    6
    def num_flat_features(self, x):
    size = x.size()[0:]
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    同时,为了匹配,我将第一个全连接层的输入乘上了64。其实这个时候我已经开始怀疑这个64是哪来的了,为什么这个张量第一维尺度有64。
    直到后来平台报错,我才意识到这个表示的不是数据的维度,而是我设计的batch size。
    为此我将上面的代码调整如下:

    1
    2
    3
    4
    5
    6
    def num_flat_features(self, x):
    size = x.size()[1:]
    num_features = 1
    for s in size:
    num_features *= s
    return num_features

    如此,问题得到解决,最终的输出应该是batch size乘上总类别数的一个张量。


arXiv

文前提到了上arXiv下论文要科学上网的事情,后来我发现了一个中科院理论物理所的一个备选镜像,但是好像不是特别稳定,不过还是先留在这里吧,万一的话可以拿来试试。
一般一些科研工作者会在论文发布之前上传到arXiv以防止自己的idea被别人用了。估计主要是为了防止类似牛顿莱布尼兹之争这种事吧。


碰到底线咯 后面没有啦

本文标题:deep learning笔记:记首次ResNet实战

文章作者:高深远

发布时间:2020年01月13日 - 17:47

最后更新:2020年02月15日 - 22:10

原始链接:https://gsy00517.github.io/deep-learning20200113174731/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:端到端学习 | 高深远的博客

deep learning笔记:端到端学习

以前觉得深度学习就是有很多层的神经网络,或者周志华提出的深度随机森林,总之只要是有很“深”的结构就是深度学习。直到不久前一位计科大佬告诉我深度学习是end-to-end(也表示成“e2e”)的,当时听的也是一知半解,回去查了一下后终于恍然大悟。本文主要基于Andrew Ng的课程中“What is end-to-end deep learning?”和“Whether to use end-to-end learning?”两节。推荐可以去看一看,讲得可以说是很浅显易懂了。


什么是end-to-end learning

传统机器学习的流程往往由多个独立的模块组成,比如在一个典型的自然语言处理(Natural Language Processing)问题中,包括分词、词性标注、句法分析、语义分析等多个独立步骤,每个步骤是一个独立的任务,其结果的好坏会影响到下一步骤,从而影响整个训练的结果,这就是非端到端的。
而深度学习模型在训练过程中,从输入端(输入数据)到输出端得到一个预测结果,该结果与真实结果相比较会得到一个误差,这个误差将用于模型每一层的调整(比如反向传播),这种训练直到模型收敛或达到预期的效果才结束,这就是端到端(end-to-end)的。

相比传统方法每一个模块都有较为明确的输出,端到端的深度学习更像是一个神秘的整体。通俗的说,端到端的深度学习能够让“数据说话”。不过这种方法是很吃数据的,因此还不至于在每个领域都胜过甚至代替传统的机器学习方法。
由于以前被标注的数据没有那么丰富,因此经典机器学习方法始终占据主流。随着近年来一个又一个数据集的出现,这种状况发生了转变。一个重要的转折点就是AlexNet的横空出世,详见deep-learning笔记:开启深度学习热潮——AlexNet
在目标跟踪领域,继相关滤波大火之后,也出现了很多优秀的深度学习算法。其中,孪生网络充分借鉴了两者的优势,取得了不错的成绩。

上面是SiameseFC的主体架构,它借用了神经网络去提取特征,而不是利用一些较为经典的特征。实际上,也可以认为它是端到端的,在调整了相关滤波的形式之后,使相关滤波的操作过程可求导,从而实现了整个模型内部的前向传播和反向传播,实现端到端。
那么,这样做有什么意义呢?误差理论告诉,误差传播的途径本身会导致误差的累积,多个阶段大概率会导致误差累积,而端到端的训练就能减少误差传播的途径,实现联合优化。


何时该用end-to-end learning

相比之下,端到端学习省去了每一步中间的数据处理和每一步模型的设计(这往往会涉及相当多的专业知识),但是端到端学习也有两个重要的缺点。

  1. 缺点一:需要大量的数据

    Andrew Ng在视频课程中举了一个例子:百度的门禁系统可以识别靠近的人脸并放行。
    如果直接使用端到端学习,那么需要训练的数据集就是一系列照片或者视频,其中人会随机出现在任何位置、任何距离等等,而这样标注好的数据集是很匮乏的。
    但是,如果我们把这个任务拆解成两个子任务。首先,在照片或者视频中定位人脸,然后放大(使人脸居中等);其次,对放大好的人脸再进行检验。这两种任务都有非常丰富的数据集或者方法可供使用。实际上,我觉得可以应用两个端到端的模型来解决这两个问题,但合起来就不是端到端的了。但在目前现有数据量的情况下,这依然能比直接端到端的方法表现得好。
  2. 缺点二:可能排除有用的人工设计

    前面提到,人工设计的模块往往是基于知识的。而知识的注入有时候会大大简化模型(尤其是数据不足的时候)。这里Andrew Ng又举了一个例子:通过X光片来估计年龄。
    传统的方法就是照一张图片,然后分割出每一块骨头并测量长度,然后通过这些长度结合理论和统计来估计年龄。
    而若是使用端到端的模型,就是直接建立图片与年龄之间的联系,这显然是很难且很复杂的,训练结果的表现也可想而知。

端到端学习确实在很多领域都能取得state-of-the-art的表现,但何时使用还是要具体问题具体分析。


神经网络学到了什么

我在前文中写了这样一句话:“相比传统方法每一个模块都有较为明确的输出,端到端的深度学习更像是一个神秘的整体”。但实际上一些研究者通过分离观察每一层,发现e2e的神经网络的确还是学到了一点东西的。
一般而言,神经网络前几层学到的内容包含的信息比较丰富具体。越到后面越抽象,即越到后面包含的语义信息越多。下面是对每一个卷积核(神经元)做可视化处理,左图为靠前的某层的可视化结果,而右图为靠后的某层的可视化结果。可以看到,相较于后面的层,前几层的卷积核似乎呈现出更明确的任务或者说功能。它们通常会找一些简单的特征,比如说边缘或者颜色阴影。

我们可以对第一层卷积层做特征可视化来看一下。

从特征可视化结果中看,出第一层卷积提取出了不同的特征,有些突出了斑马的形状,有些突出了背景,有些突出了斑马的斑纹等。
下面是Andrew Ng在课程中举得一个可视化例子,他所采用的方法是对每层中的隐藏单元用数据集去遍历,并且寻找出9个使得隐藏单元有较大的输出或是较大的激活的图片或者图像块。注意网络层数越深其感受野会越大。详见deep-learning笔记:着眼于深度——VGG简介与pytorch实现

实际上,实现可视化的方法有多种,可以利用反卷积,也可以对一系列图像求响应值,上面的两个例子就是用了不同的方法(前者是针对一张图,而Andrew Ng用了一个数据集)。在Visualizing and Understanding Convolutional Networks一文中,作者也提出了一些更复杂的方式来可视化卷积神经网络的计算。我在computer-vision笔记:上采样和下采样中,也对反卷积进行了介绍。
在NLP领域,也有类似的发现,比如一些训练过后的神经元对特定标点的响应特别强烈,而有一些训练过后的神经元对一些特定语气词的响应特别强烈。


碰到底线咯 后面没有啦

本文标题:deep learning笔记:端到端学习

文章作者:高深远

发布时间:2020年01月22日 - 16:45

最后更新:2020年02月02日 - 14:59

原始链接:https://gsy00517.github.io/deep-learning20200122164503/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:端到端学习 | 高深远的博客

deep learning笔记:端到端学习

以前觉得深度学习就是有很多层的神经网络,或者周志华提出的深度随机森林,总之只要是有很“深”的结构就是深度学习。直到不久前一位计科大佬告诉我深度学习是end-to-end(也表示成“e2e”)的,当时听的也是一知半解,回去查了一下后终于恍然大悟。本文主要基于Andrew Ng的课程中“What is end-to-end deep learning?”和“Whether to use end-to-end learning?”两节。推荐可以去看一看,讲得可以说是很浅显易懂了。


什么是end-to-end learning

传统机器学习的流程往往由多个独立的模块组成,比如在一个典型的自然语言处理(Natural Language Processing)问题中,包括分词、词性标注、句法分析、语义分析等多个独立步骤,每个步骤是一个独立的任务,其结果的好坏会影响到下一步骤,从而影响整个训练的结果,这就是非端到端的。
而深度学习模型在训练过程中,从输入端(输入数据)到输出端得到一个预测结果,该结果与真实结果相比较会得到一个误差,这个误差将用于模型每一层的调整(比如反向传播),这种训练直到模型收敛或达到预期的效果才结束,这就是端到端(end-to-end)的。

相比传统方法每一个模块都有较为明确的输出,端到端的深度学习更像是一个神秘的整体。通俗的说,端到端的深度学习能够让“数据说话”。不过这种方法是很吃数据的,因此还不至于在每个领域都胜过甚至代替传统的机器学习方法。
由于以前被标注的数据没有那么丰富,因此经典机器学习方法始终占据主流。随着近年来一个又一个数据集的出现,这种状况发生了转变。一个重要的转折点就是AlexNet的横空出世,详见deep-learning笔记:开启深度学习热潮——AlexNet
在目标跟踪领域,继相关滤波大火之后,也出现了很多优秀的深度学习算法。其中,孪生网络充分借鉴了两者的优势,取得了不错的成绩。

上面是SiameseFC的主体架构,它借用了神经网络去提取特征,而不是利用一些较为经典的特征。实际上,也可以认为它是端到端的,在调整了相关滤波的形式之后,使相关滤波的操作过程可求导,从而实现了整个模型内部的前向传播和反向传播,实现端到端。
那么,这样做有什么意义呢?误差理论告诉,误差传播的途径本身会导致误差的累积,多个阶段大概率会导致误差累积,而端到端的训练就能减少误差传播的途径,实现联合优化。


何时该用end-to-end learning

相比之下,端到端学习省去了每一步中间的数据处理和每一步模型的设计(这往往会涉及相当多的专业知识),但是端到端学习也有两个重要的缺点。

  1. 缺点一:需要大量的数据

    Andrew Ng在视频课程中举了一个例子:百度的门禁系统可以识别靠近的人脸并放行。
    如果直接使用端到端学习,那么需要训练的数据集就是一系列照片或者视频,其中人会随机出现在任何位置、任何距离等等,而这样标注好的数据集是很匮乏的。
    但是,如果我们把这个任务拆解成两个子任务。首先,在照片或者视频中定位人脸,然后放大(使人脸居中等);其次,对放大好的人脸再进行检验。这两种任务都有非常丰富的数据集或者方法可供使用。实际上,我觉得可以应用两个端到端的模型来解决这两个问题,但合起来就不是端到端的了。但在目前现有数据量的情况下,这依然能比直接端到端的方法表现得好。
  2. 缺点二:可能排除有用的人工设计

    前面提到,人工设计的模块往往是基于知识的。而知识的注入有时候会大大简化模型(尤其是数据不足的时候)。这里Andrew Ng又举了一个例子:通过X光片来估计年龄。
    传统的方法就是照一张图片,然后分割出每一块骨头并测量长度,然后通过这些长度结合理论和统计来估计年龄。
    而若是使用端到端的模型,就是直接建立图片与年龄之间的联系,这显然是很难且很复杂的,训练结果的表现也可想而知。

端到端学习确实在很多领域都能取得state-of-the-art的表现,但何时使用还是要具体问题具体分析。


神经网络学到了什么

我在前文中写了这样一句话:“相比传统方法每一个模块都有较为明确的输出,端到端的深度学习更像是一个神秘的整体”。但实际上一些研究者通过分离观察每一层,发现e2e的神经网络的确还是学到了一点东西的。
一般而言,神经网络前几层学到的内容包含的信息比较丰富具体。越到后面越抽象,即越到后面包含的语义信息越多。下面是对每一个卷积核(神经元)做可视化处理,左图为靠前的某层的可视化结果,而右图为靠后的某层的可视化结果。可以看到,相较于后面的层,前几层的卷积核似乎呈现出更明确的任务或者说功能。它们通常会找一些简单的特征,比如说边缘或者颜色阴影。

我们可以对第一层卷积层做特征可视化来看一下。

从特征可视化结果中看,出第一层卷积提取出了不同的特征,有些突出了斑马的形状,有些突出了背景,有些突出了斑马的斑纹等。
下面是Andrew Ng在课程中举得一个可视化例子,他所采用的方法是对每层中的隐藏单元用数据集去遍历,并且寻找出9个使得隐藏单元有较大的输出或是较大的激活的图片或者图像块。注意网络层数越深其感受野会越大。详见deep-learning笔记:着眼于深度——VGG简介与pytorch实现

实际上,实现可视化的方法有多种,可以利用反卷积,也可以对一系列图像求响应值,上面的两个例子就是用了不同的方法(前者是针对一张图,而Andrew Ng用了一个数据集)。在Visualizing and Understanding Convolutional Networks一文中,作者也提出了一些更复杂的方式来可视化卷积神经网络的计算。我在computer-vision笔记:上采样和下采样中,也对反卷积进行了介绍。
在NLP领域,也有类似的发现,比如一些训练过后的神经元对特定标点的响应特别强烈,而有一些训练过后的神经元对一些特定语气词的响应特别强烈。


碰到底线咯 后面没有啦

本文标题:deep learning笔记:端到端学习

文章作者:高深远

发布时间:2020年01月22日 - 16:45

最后更新:2020年02月02日 - 14:59

原始链接:https://gsy00517.github.io/deep-learning20200122164503/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:迁移学习 | 高深远的博客

deep learning笔记:迁移学习

迁移学习(Transfer Learning),又称预训练。即利用社区内开源的权重参数更快更好地训练自己的网络。然而,我一直纳闷的是,别人训练好的参数是怎么直接用到自己的网络上来的,倘若网络结构内部有一点不同那岂不是完全不一样了吗?Andrew Ng的课程给了我很大的启发,结合上自己的一些想法,写下来。

References

电子文献:
http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/#Network_Architecture


原理

这里摘录上面参考资料中的一段话,我觉得说得很明白了。
Using a network trained on one dataset on a different problem is possible because neural networks exhibit “transfer learning”. The first few layers of the network learn to detect general features such as edges and color blobs that are good discriminating features across many different problems. The features learnt by the later layers are higher level, more problem specific features. These layers can either be removed or the weights for these layers can be fine-tuned during back-propagation.


如何使用迁移学习

迁移学习主要有如下三种策略。

下面我们具体情况具体分析。

  1. 训练数据较少

    首先,我们把开源的代码(即网络结构)和对应的权重都下载下来。当手头的训练集较小时,我们可以冻结所有层的参数,去掉网络中的softmax层或者其它与最后输出相联系的层,并且创建自己的softmax单元。这主要是考虑到下载的模型所对应的输出类别或者其他的需求与我们的不一致。在训练过程中,我们保持之前所有层冻结(用于特征提取等),只训练和我们自己设计的softmax层有关的参数。
    其实这就相当于用预训练的网络构成一个映射关系,对每一个输入都能产生一个特征向量。然后用自己设计的一个很浅的softmax网络对这些特征向量做预测。

    注:这些特征向量可以存到硬盘中,以节约每次都要遍历训练集重新计算这个激活值的时间。这在用Siamese网络进行人脸识别时是一个较为常用的操作。

  2. 训练数据中等

    如果数据较多,我们可以冻结较少的层。根据上文所述的原理,我们一般冻结较为基础的前面的几层。对于后面的层,我们有两种方法。
    (1)可以加载权重作为初始化,然后用同样的结构和自己的数据集继续训练。
    (2)也可以直接去掉这几层,换成我们自己的隐藏单元和自己的输出层。
    其实我觉得这里的基本思想还是相当于把冻结的那几层看成一个关于特征的映射。
  3. 训练数据较多

    若有足够多的数据用来作训练集,我们这时就可以把所有的参数都仅用来初始化,然后训练整个网络。因为此时不用担心过拟合的问题。
    其实规律就是:拥有越多的数据,我们需要冻结的层数(参数)越少,我们能够训练的层数(参数)就越多。

除了训练数据量之外,新数据集与原数据集的相似度也是一个需要考虑的点。倘若我们拥有较多的数据,而新数据集与原数据集的相似度却很低时,我们最好不要使用迁移学习进行预训练,而是从头开始训练整个网络。


为什么要迁移学习

关于使用迁移学习的原因,Andrew Ng没有提及,不多简单想来主要的原因主要有如下几点:

  1. 迁移学习可以弥补训练数据的不足。
  2. 迁移学习可以大大减少训练时间。
  3. 迁移学习(特别是同一领域相关的参数)可以有效地防止梯度下降卡在局部最优解处,一定程度上保证了算法的收敛性。

碰到底线咯 后面没有啦

本文标题:deep learning笔记:迁移学习

文章作者:高深远

发布时间:2020年01月28日 - 14:36

最后更新:2020年02月15日 - 22:09

原始链接:https://gsy00517.github.io/deep-learning20200128143652/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:迁移学习 | 高深远的博客

deep learning笔记:迁移学习

迁移学习(Transfer Learning),又称预训练。即利用社区内开源的权重参数更快更好地训练自己的网络。然而,我一直纳闷的是,别人训练好的参数是怎么直接用到自己的网络上来的,倘若网络结构内部有一点不同那岂不是完全不一样了吗?Andrew Ng的课程给了我很大的启发,结合上自己的一些想法,写下来。

References

电子文献:
http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/#Network_Architecture


原理

这里摘录上面参考资料中的一段话,我觉得说得很明白了。
Using a network trained on one dataset on a different problem is possible because neural networks exhibit “transfer learning”. The first few layers of the network learn to detect general features such as edges and color blobs that are good discriminating features across many different problems. The features learnt by the later layers are higher level, more problem specific features. These layers can either be removed or the weights for these layers can be fine-tuned during back-propagation.


如何使用迁移学习

迁移学习主要有如下三种策略。

下面我们具体情况具体分析。

  1. 训练数据较少

    首先,我们把开源的代码(即网络结构)和对应的权重都下载下来。当手头的训练集较小时,我们可以冻结所有层的参数,去掉网络中的softmax层或者其它与最后输出相联系的层,并且创建自己的softmax单元。这主要是考虑到下载的模型所对应的输出类别或者其他的需求与我们的不一致。在训练过程中,我们保持之前所有层冻结(用于特征提取等),只训练和我们自己设计的softmax层有关的参数。
    其实这就相当于用预训练的网络构成一个映射关系,对每一个输入都能产生一个特征向量。然后用自己设计的一个很浅的softmax网络对这些特征向量做预测。

    注:这些特征向量可以存到硬盘中,以节约每次都要遍历训练集重新计算这个激活值的时间。这在用Siamese网络进行人脸识别时是一个较为常用的操作。

  2. 训练数据中等

    如果数据较多,我们可以冻结较少的层。根据上文所述的原理,我们一般冻结较为基础的前面的几层。对于后面的层,我们有两种方法。
    (1)可以加载权重作为初始化,然后用同样的结构和自己的数据集继续训练。
    (2)也可以直接去掉这几层,换成我们自己的隐藏单元和自己的输出层。
    其实我觉得这里的基本思想还是相当于把冻结的那几层看成一个关于特征的映射。
  3. 训练数据较多

    若有足够多的数据用来作训练集,我们这时就可以把所有的参数都仅用来初始化,然后训练整个网络。因为此时不用担心过拟合的问题。
    其实规律就是:拥有越多的数据,我们需要冻结的层数(参数)越少,我们能够训练的层数(参数)就越多。

除了训练数据量之外,新数据集与原数据集的相似度也是一个需要考虑的点。倘若我们拥有较多的数据,而新数据集与原数据集的相似度却很低时,我们最好不要使用迁移学习进行预训练,而是从头开始训练整个网络。


为什么要迁移学习

关于使用迁移学习的原因,Andrew Ng没有提及,不多简单想来主要的原因主要有如下几点:

  1. 迁移学习可以弥补训练数据的不足。
  2. 迁移学习可以大大减少训练时间。
  3. 迁移学习(特别是同一领域相关的参数)可以有效地防止梯度下降卡在局部最优解处,一定程度上保证了算法的收敛性。

碰到底线咯 后面没有啦

本文标题:deep learning笔记:迁移学习

文章作者:高深远

发布时间:2020年01月28日 - 14:36

最后更新:2020年02月15日 - 22:09

原始链接:https://gsy00517.github.io/deep-learning20200128143652/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:元学习 | 高深远的博客

deep learning笔记:元学习

元学习(Meta Learning)即让机器学习如何去学习(learn to learn)。我前几天看到这个概念,觉得非常有意思,这几天也一直在看相关的概念和implement,发现其实很多方法或者思想已经在最新的一些工作中得到有意或者无意运用。因此我决定对我目前的了解做一个总结,或许能给之后的思考带来启发。
强烈推荐台湾教授李宏毅关于Meta Learning的课程录像,油管上有资源,总共4个part,讲得真的非常清楚!
此外,斯坦福在2019年秋季也推出了多任务学习和元学习的CS330,感兴趣可以看一看,语速稍快,可以锻炼一下英语。
由于目前对这个领域的了解还不够深入,分类可能不是很恰当,以后待理解加深之后再做调整,如有错误还请见谅。

References

电子文献:
https://cloud.tencent.com/developer/article/1463397
https://zhuanlan.zhihu.com/p/28639662
https://mp.weixin.qq.com/s/MApZS0-SL4FNP2kmavhcxQ
https://blog.csdn.net/liuglen/article/details/84770069
https://blog.csdn.net/mao_feng/article/details/78939864
https://blog.csdn.net/ppp8300885/article/details/80383246


元学习

人工智能的一个基本问题是它无法像人类一样高效地学习。
与人类相比,大多数最先进的深度学习方法都有两个关键的弱点:

  1. 样本效率低

    深度学习的样本效率很差,学习效率很低下。尽管目前已经有许多深度学习模型显示了超人的表现,但达到这种效果需要数百万个训练样本来训练。也就是说对one-shot learning和few-shot learning的能力还是不够强。
  2. 可移植性差

    许多模型不会从以前的经验或学到的知识中学习。也就是说训练得到的知识是不共享的,并且每个任务都独立于其他任务进行训练。

基于以上这些问题的考虑,“元学习”被提出,它研究的不是如何提升模型解决某项具体的任务(分类、回归、检测)的能力,而是研究如何提升模型解决一系列任务的能力。它是增强学习之后的一个研究分支,总的来看,人工智能的研究呈现出了从Artificial Intelligence,到Machine Learning,到Deep Learning,到Deep Reinforcement Learning,再到Deep Meta Learning这样一个大体的方向,这样下去或许最后就真的能够创造出自己学习的机器了。也就是说:
在过去,我们不仅知道机器是怎么想的,而且知道机器是怎么学的。
目前,我们虽然不知道机器是怎么想的,但是知道机器是怎么学的。
未来,我们非但不知道机器是怎么想的,还不知道机器是怎么学的。
也就是从人工智能到智能。
哈哈上面扯远了,前面说过,我们可以将元学习定义为“学习如何学习”。但是实际上,我们还不知道确切的定义或解决方案。因此,它仍然是一个宽松的术语,指的是不同的方法。比如,从某种意义上来说,我们可以认为迁移学习也是一种元学习。它利用别的任务中训练得比较好的参数,使得在本任务得数据集上能够更好更快地达到最优。
一般可以将Meta Learning分成两个阶段:

  1. Gradual Learning

    也就是通过在一些相似任务上训练得到Meta Learner,它具有较强的泛化能力。
  2. Rapid Learning

    也就是利用该Meta Learner直到之后的学习过程,使模型能够快速调整以适应新的任务。

在李宏毅的课程中提到,Machine Learning其实就是学习如何根据数据找出一个$f$函数,使得在测试中,该函数能根据输入获得正确的输出。而Meta Learning其实就是找到一个$F$函数,这个$F$函数的能力是找出$f$函数,也就是说,不同于机器学习直接找$f$,元学习中机器的任务是去找这个$F$。
来看下图,这里说的其实就是一个训练的过程,通过训练找出最终模型中效果最好的参数$\widehat{\theta }$。

可以看上图红框圈出的部分,仔细想想,这些部分的不同也就构成了不同的算法。然而,目前这些红框中的结构基本上还是人为设计定义的。那么元学习思考的就是能不能让机器自己学习其中的方法与结构。


数据集划分

由于元学习学习的是解决一系列任务的能力,因此不同于以往的将数据集划分成训练集和测试集(或许还有验证集),在元学习中,我们将数据集分割成Training Tasks、Testing Tasks(、Validaiton Tasks)。每一个任务都包含一个训练集和一个测试集。为了区别,这里称这些小训练集为Support Set,称这些小测试集为Query Set。


元学习与one-shot learning

一般来说,元学习总是与one-shot learning相结合的。我们知道,目前许多单任务模型的训练都是以天计数的。而元学习的训练集中每一组数据都是一个任务,假如Training Tasks中有n个任务,那就是说我们要训练n次也就是单任务模型的训练时间的n倍,这也意味着我们需要很长的时间才能检验一次效果,显然是不划算的。
而one-shot learning虽然只提供了少量的数据,但这也是它的一个优势——训练更快,因此我们对one-shot learning任务进行元学习,可以在较短的时间内看到训练效果。


学习初值的元学习

上文说过可以让机器自己学习红框中的结构与方法,这里有元学习中两种代表性的方法MAML和Reptile,它们都学习的是初始化的方式(上图第二个红框)。MAML音同mammal(哺乳动物),也是Model Agnostic Meta Learning的缩写;而Reptile(意为爬行动物),由于找不到缩写的来源,严重怀疑是故意这么命名来与MAML对应。
它们两者实际上学习的是该把初始化参数$\phi $设成什么,其损失函数为

这里的$\widehat{\theta }^{n}$其实就是在第n个任务中,由初始参数$\phi $通过Support Set学到的最终模型中的参数。而$l^{n}\left ( \widehat{\theta }^{n} \right )$就是对于第n个任务在Query Set上的损失。
首先我借用李宏毅老师给的例子来弄明白这里“学习把参数$\phi $初始化成什么”到底与普通的训练中“学习把参数训练成什么”有什么区别。

在上图中,左图表示的是我们一般意义上的训练(Model Pre-training),右图表示的是MAML或者Reptile中的初始化。可以看到,左图最终训练得出的参数$\phi $在task 1(Training Tasks)上表现得最好,但在一个不同的任务task 2(Testing Tasks)上,它只能收敛到局部最优点(local minima)。在右图中,虽然在Training Tasks上表现得不是最好,但是在遇到新的任务训练时,现在的初始化参数$\phi $可以很快很好地收敛到全局最优点(global minima)。
也就是说,一般意义上的训练,我们关注的是在所有Training Tasks上达到最好的情况,比如找出让训练时的损失函数最小的一组参数,但我们并不关注拿此时的参数去新任务上进行训练是否会得到好的结果;而在学习初始化的元学习中,我们不在意$\phi $在Training Tasks中表现得有多好,我们在意的是拿初始化后的$\phi $到下一个任务上去训练能够很快很好地取得不错的表现。简而言之,Model Pre-training关注的是$\phi $当前的表现,而Meta Learning关注的是$\phi $未来遇到新任务时的潜力。如果说到这里还不是很清楚,后文还有直观的理解。
具体怎么找到这个$\phi $呢?这就产生了下面两种方法。

  1. MAML模型无关元学习

    MAML考虑的是训练出一个初始化参数$\phi $,使得拥有这个参数的模型在遇到一个新的任务时能够一步到位达到最好,比如接收到一张新的图片就可以快速训练成为辨别该图片同一类别图片的最优模型。
    当然,寻找这个$\phi $的方法还是梯度下降,因此也要求$\phi $关于损失函数$L$的偏导数。由于MAML考虑的是模型在遇到新任务时一步到位,因此$\widehat{\theta }$也仅进行一次梯度下降,如下所示。 由于这里$\widehat{\theta }_{j}$对$\phi _{i}$的偏导数还是要分类讨论很麻烦,因此作者使用了a first-order approximation,如下图所示。 也就是当且仅当$i$等于$j$的时候时取$\widehat{\theta }_{j}$对$\phi _{i}$的偏导数为1,否则取偏导数为0。
    我们可以来直观地看一下MAML在做什么。如下图所示,左为MAML,右为Model Pre-training,上文已比较过两者关注点的不同,这里再看看两者方法上的不同。 MAML首先在第一个任务采样出来的样本上计算损失函数并进行一次梯度下降,然后用此时的参数计算损失函数再进行一次梯度下降,最后,根据第二次梯度下降的梯度方向,平行地对原参数$\phi $进行一次梯度下降。注意,这里每一次梯度下降使用的学习率不需要一样。可以这样理解,第二次梯度下降后的参数相比第一次梯度下降后的参数在task m上表现要好,因此$\phi $沿相同的方向梯度下降,就能使在遇到task m的时以更少的训练次数到达离较优的第二次梯度下降后的参数更近的位置。在task n上同理。
    而Model Pre-training,则是要求在训练的每个任务上尽可能做到最好,于是直接朝着由每个任务采样样本得到的损失函数计算出的梯度的方向靠近。
    其实,Model Pre-training的基本思想就是每训练完一个任务,我们就把这过程中的信息用来更新模型。而Meta Learning的基本思想就是找到所有学习任务背后的基础知识。因此,我们不会立即更新模型,而是等待一批任务完成。稍后,我们将从这些任务中学到的所有知识合并到一个更新中。 个人感觉,这有点像想要去做、却为了大局而保持一点克制的感觉,似乎的确蕴含了一点点人生哲理在里面。
  2. Reptile一阶元学习

    Reptile的思想其实是和MAML一样的,只不过它使用了多次梯度下降后的方向来更新$\phi _{i}$,也就是对遇到一个新任务时的更新次数不加限制,不指望一步到位。 我们可以通过Pre-training、MAML和Reptile的对比图来理解一下Reptile改进的思想,它相当于在当下的任务和未来的任务之间寻找一个平衡。 我觉得就泛化能力而言,MAML可能还是要好于Reptile的。
    实际上,在MAML的使用中,我们也可以进行多次梯度下降,训练时假设一步到位并不意味着在实际使用的时候只能使用一次梯度下降。

李宏毅老师在课程中还给出了一个toy example的例子,区分Model Pre-training和Meta Learning。如下所示,toy example的目标是拟合正弦曲线

这里的$a$和$b$是任取的随机数,也就是每个任务都是拟合一个不同的正弦曲线。
来看看两种方法的训练结果,其中左边是Model Pre-training的结果,右边是Meta Learning的结果,这里使用的是MAML方法。

图中绿色的线就是训练好之后的参数构成的模型。可以看到,Model Pre-training为了在训练集中各种正弦曲线中达到损失函数最小,训练结果为一条直线,这在测试时效果很差;而MAML则预备在遇到一个新的曲线时能很快地去拟合,在测试时能很快地达到很好的效果。
然而,目前这类方法的效果好像仅在多样性较低的任务分布中进行实验得到了验证,在这些任务上做得好不代表在复杂程度不同、模式不同的任务上也可以有很好的表现。对于应用在高复杂度的任务上的效果可能还是要打个问号的,在这方面的研究还有很长的路要走。

补充:其实这两种方法后面还是有比较复杂的数学推导的,我在网上看到一个博客写得挺好,补充在这里MAML-模型无关元学习算法Reptile-一阶元学习算法


学习更新的学习

上一节介绍的是如何让机器自己学习出一个初始化的方法,前文圈出的红框中另有一项是Update,那么我们是否可以学习如何更新模型呢?

借鉴LSTM

之前没有看过LSTM,所以我想顺便也理一下LSTM的基本思路。

相比于RNN,LSTM从前一级获得的输入有两个(也可以看作将一个输入拆成两段),区别就在于$c$更新得更慢,$h$更新得更快。

这里各个参数表示的含义如下所示,$\sigma $表示的是某种函数。

每一块的输出和传递给下一级的$c$与$h$计算方法如下所示,这里表示的是点乘操作。

  1. 方法1

    一般而言,我们梯度下降使用的是如下公式:对比LSTM中$c$的的更新公式$c^{t}=z^{f}\odot c^{t-1}+z^{i}\odot z$,我们很容易发现两者的结构有相通之处,由此产生灵感,我们是否可以把这里的$z^{f}$和$z^{i}$也变成可以学习的呢。也就是输入$\theta ^{t-1}$和$-\bigtriangledown _{\theta }l$,不人为设定$z^{f}$和$z^{i}$,直接输出$\theta ^{t}$。
    于是就有了如下方法。 但是,由于$-\bigtriangledown _{\theta }l$的计算依赖于$\theta ^{t-1}$(上图虚线表示),这在反向传播时会比较复杂,因此为了简化,在反向传播时不考虑$-\bigtriangledown _{\theta }l$对$\theta ^{t-1}$的依赖。 注意,如果对所有的参数都设计这样一种学习如何更新的方式,那么计算量是非常大的,因此我们往往对所有参数采用同一套学习出来的更新方式。这是合理的,因为往往我们也会对所有参数设置同样的学习率。
    作者对此进行实验来看一看$z^{f}$和$z^{i}$的学习结果究竟如何。如下所示,可见$z^{f}$最后还是收敛到了1,而$z^{i}$也集中在1附近,但也的确学到了一些不一样的信息。 该实验可以表明,我们一般使用的梯度下降公式是比较合理的。不过这里元学习的方法并不是没有作用,后面会提到它的效果。
  2. 方法2

    为了利用更多以往的“记忆”,也可以设计一种两层的LSTM,如下图所示。 实际上,LSTM这种循环序列模型本身就有利用过往记忆的功能,我将在下一节做更多的介绍。
  3. 效果

    其实这些方法还是比较难实现的,这里来看一下利用LSTM来更新参数最后的效果。 可以看到,这种方法在各种优化算法面前还是有明显的优势的。这里有一个问题是作者进行的实验都是在MNIST数据集上进行的,比如说这里Meta Learning的Training Tasks是识别0,1,2,3…7,Testing Tasks是识别8,9。其实这些任务是同一类任务。作者也注意到了这一点,因此他在训练时用的是20units,测试时使用了40units;训练时只用了1个layer,测试时用了2个layer。以此来证明利用LSTM来更新参数这种方法在不同的模型中的能力。

    预测梯度

    既然Meta Learning的目的是实现快速学习,而快速学习的关键就是神经网络的梯度下降要准且快,那么是否可以让神经网络利用以往的任务学习如何预测梯度,这样面对新的任务,只要梯度预测得准,那么学习得就会更快。
    在Learning to Learn by Gradient Descent by Gradient Descent一文中(好好品味这个题目),作者提出了这样一种方法,即训练一个通用的神经网络来预测梯度,用一次二次方程的回归问题来训练,这种方法得到的神经网络优化算法比Adam、RMSProp还要好。

学习记忆的学习

元学习其实也可以利用以往的经验来学习,于是这又诞生了一系列模型。

记忆增强神经网络

一般的神经网络不具有记忆功能,输出的结果只基于当前的输入。而LSTM网络的出现则让网络有了记忆,它能够根据之前的输入给出当前的输出。但是,LSTM的记忆程度并不是那么理想,对于比较长的输入序列,LSTM的最终输出只与最后的几步输入有关,也就是long dependency问题,当然这个问题可以由注意力机制解决,然而还是不能从根本上解决长期记忆的问题,原因是由于LSTM是假设在时间序列上的输入输出:由t-1时刻得到t时刻的输出,然后再循环输入t时刻的结果得到t+1时刻的输出,这样势必会使处于前面序列的输入被淹没,导致这部分记忆被“丢掉”。
基于以上问题,研究者们提出了神经图灵机等记忆增强神经网络模型。我们知道,人除了用脑子记,还会使用做笔记等方式做辅助,当我们忘记时,我们可以去看笔记来获得相关的记忆。记忆增强神经网络的思想就是如此,它让所有的笔记的“读”“写”操作都可微分化,因此可以用神经网络误差后向传播的方式去训练模型。
在记忆增强神经网络中,我们引入外部存储器来存储样本表示和类标签信息。以Meta-Learning with Memory-augmented Neural Networks这篇文章为例,我们看一下它的网络结构。

我们可以看到,网络的输入把上一次的y label也作为输入,并且添加了external memory存储上一次的x输入,这使得下一次输入后进行反向传播时,可以让y label和x建立联系,使得之后的x能够通过外部记忆获取相关图像进行比对来实现更好的预测。

WaveNet

WaveNet的网络每次都利用了之前的数据,因此可以考虑WaveNet的方式来实现Meta Learning。

SNAIL

SNAIL意为蜗牛(为什么这么多动物名…),其实直接把论文标题缩写之后和SNAIL是不一样的,之所以命名成SNAIL可能是因为网络结构有点像蜗牛吧。

  1. 时序卷积

    时序卷积是通过在时间维度上膨胀的一维卷积生成时序数据的结构,如下图所示。 这种时序卷积是因果的,所以在下一个时间节点生成的值只会被之前时间节点的信息影响,而不受未来信息的影响。相比较于传统的RNN,它提供了一种更直接,更高带宽的方式来获取过去的信息。但是,为了处理更长的序列,膨胀率通常是指数级增长的,所以需要的卷积层数和序列长度呈对数关系。因此,只能对很久之前的信息进行粗略的访问,有限的容量和位置依赖性对于元学习方法是不利的,不能充分利用大量的先前的经验。
  2. 注意力模块

    注意力模块可以让模型在可能的无限大的上下文中精确的定位信息,把上下文信息当做无序的键值对,通过内容对其进行查找。
  3. 网络

    SNAIL由两个时序卷积和attention交错组成。时序卷积可以在有限的上下文中提供高带宽的访问方式,attention可以在很大的上下文中精确地访问信息,所以将二者结合起来就得到了SNAIL。在时序卷积产生的上下文中应用causal attention,可以使网络学习应该挑选聚集哪些信息,以及如何更好地表示这些信息。

    注意:英语小贴士,因果:causal;休闲:casual。


学习方法的学习

元学习还用于对数据处理中的一些方法进行学习,比如这里要介绍的对特征提取方法的学习。

Siamese Network孪生网络

Siamese Network中文译为孪生网络。常用于人脸验证(不同于人脸识别),近几年在目标跟踪领域也大放光彩。
在Siamese Network中,我们首先使用两个共享模型参数值的相同网络来提取两个样本的特征(也有个别伪Siamese Network,它们不共享网络权重)。然后,我们将提取出的特征输入鉴别器,判断两个样本是否属于同一类对象。例如,我们可以计算其特征向量的余弦相似度(p)。如果它们相似,那么p应该接近1;否则,它们应该接近0。根据样本的标签和p,我们对网络进行相应的训练。简而言之,我们希望找到使样例属于同一类或将它们区分开来的特性。

这里利用神经网络的好处是它能够学习到哪些特征是该提取的,比如在一些传统的方法中,它们会对整个图像进行特征转化,其中也将包括背景,这样就可能会导致当两个相似背景但不同对象的图片作为输入时,输出会判定为相同。而神经网络它可以在训练过程中学习到哪些部分是值得关注的,可以学会ignore背景。
李宏毅老师在讲这一块的时候还提到了一种思路,就是学习一种算法(生成网络),这种算法能够使用one-shot learning输入的少量样本生成许多样本用于和prediction的那个输入进行比对验证,其框架与Siamese Network基本相似。

Matching Network匹配网络

Matching Network与Siamese Network十分相似。我们知道,人的注意力是可以利用以往的经验来实现提升的。那么,能不能利用以往的任务来训练一个attention模型,使得模型面对新的任务时,能够直接关注最重要的部分。
于是就有了Matching Network。

这里的g和f是特征提取器,和Siamese Network一样使用深度网络来提取特征,用于我们的输入和测试样本。通常情况下,g和f是相同的,共享相同的深度网络。然后,我们比较它们的相似度。同样,我们从预测中计算一个损失函数来训练我们的特征提取器。
这里构造了一个attention机制,也就是最后的label判断是通过attention的叠加得到的:

其实这里的网络结构远比上面画出来的复杂(用到了LSTM)。我们可以来看一下它的表现,在Omniglot数据集(找一批人照着象形文字画出来的)上,它取得了比Siamese Network更好一点的成绩。不过具体能力如何,还得看论文跑实验。

Prototypical Network原型网络

该方法思想十分简单,效果也非常好。它学习的是一个度量空间,通过计算和每个类别的原型表达的距离来进行分类。
具体的思路是:每个类别都存在一个聚在某单个原型表达周围的embedding,该类的原型是Support Set在embedding空间中的均值。然后,分类问题变成在embedding空间中的最近邻。如图所示,c1、c2、c3分别是三个类别的均值中心(称Prototype),将测试样本x进行embedding后,与这3个中心进行距离计算,从而获得x的类别。


学习损失的学习

前面提到,Meta Learning处理的主要是one-shot learning,而one-shot learning需要让学习的速度更快。学习效率除了用更好的梯度来提高,也可以考虑寻找更好的损失函数。根据这种思路,或许可以构造一个模型利用以往的任务来学习如何预测loss。

如上图所示,作者构造了一个Meta-Critic Network,其作用是用来学习如何预测Actor Network的loss。这也是一种思路吧。


学习结构的学习

还有一种思路就是结合深度学习动态地生成一个新的模型。

这种方法能使得模型更加准确,但不一定能够更有效地使用较少的样本来学习,不适合用于one-shot learning。


小结

套娃预警!
在古印度神话中,有人问世界在哪?答案是在乌龟的背上。那么乌龟在哪?乌龟在另一个大乌龟的背上。那么大乌龟在哪…

那么,我们之前让机器learn,现在想办法让机器learn to learn,那之后是不是还要learn to learn to learn再learn to learn to learn to learn呢?哈哈套娃警告。
难以想象未来的机器会是怎么样的,如此智能真的有点细思极恐呢。话说回来,Meta Learning方兴未艾,各种神奇的idea层出不穷,但是真正的杀手级算法尚未出现,让我们拭目以待!


碰到底线咯 后面没有啦

本文标题:deep learning笔记:元学习

文章作者:高深远

发布时间:2020年02月03日 - 16:22

最后更新:2020年03月27日 - 11:14

原始链接:https://gsy00517.github.io/deep-learning20200203162245/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:元学习 | 高深远的博客

deep learning笔记:元学习

元学习(Meta Learning)即让机器学习如何去学习(learn to learn)。我前几天看到这个概念,觉得非常有意思,这几天也一直在看相关的概念和implement,发现其实很多方法或者思想已经在最新的一些工作中得到有意或者无意运用。因此我决定对我目前的了解做一个总结,或许能给之后的思考带来启发。
强烈推荐台湾教授李宏毅关于Meta Learning的课程录像,油管上有资源,总共4个part,讲得真的非常清楚!
此外,斯坦福在2019年秋季也推出了多任务学习和元学习的CS330,感兴趣可以看一看,语速稍快,可以锻炼一下英语。
由于目前对这个领域的了解还不够深入,分类可能不是很恰当,以后待理解加深之后再做调整,如有错误还请见谅。

References

电子文献:
https://cloud.tencent.com/developer/article/1463397
https://zhuanlan.zhihu.com/p/28639662
https://mp.weixin.qq.com/s/MApZS0-SL4FNP2kmavhcxQ
https://blog.csdn.net/liuglen/article/details/84770069
https://blog.csdn.net/mao_feng/article/details/78939864
https://blog.csdn.net/ppp8300885/article/details/80383246


元学习

人工智能的一个基本问题是它无法像人类一样高效地学习。
与人类相比,大多数最先进的深度学习方法都有两个关键的弱点:

  1. 样本效率低

    深度学习的样本效率很差,学习效率很低下。尽管目前已经有许多深度学习模型显示了超人的表现,但达到这种效果需要数百万个训练样本来训练。也就是说对one-shot learning和few-shot learning的能力还是不够强。
  2. 可移植性差

    许多模型不会从以前的经验或学到的知识中学习。也就是说训练得到的知识是不共享的,并且每个任务都独立于其他任务进行训练。

基于以上这些问题的考虑,“元学习”被提出,它研究的不是如何提升模型解决某项具体的任务(分类、回归、检测)的能力,而是研究如何提升模型解决一系列任务的能力。它是增强学习之后的一个研究分支,总的来看,人工智能的研究呈现出了从Artificial Intelligence,到Machine Learning,到Deep Learning,到Deep Reinforcement Learning,再到Deep Meta Learning这样一个大体的方向,这样下去或许最后就真的能够创造出自己学习的机器了。也就是说:
在过去,我们不仅知道机器是怎么想的,而且知道机器是怎么学的。
目前,我们虽然不知道机器是怎么想的,但是知道机器是怎么学的。
未来,我们非但不知道机器是怎么想的,还不知道机器是怎么学的。
也就是从人工智能到智能。
哈哈上面扯远了,前面说过,我们可以将元学习定义为“学习如何学习”。但是实际上,我们还不知道确切的定义或解决方案。因此,它仍然是一个宽松的术语,指的是不同的方法。比如,从某种意义上来说,我们可以认为迁移学习也是一种元学习。它利用别的任务中训练得比较好的参数,使得在本任务得数据集上能够更好更快地达到最优。
一般可以将Meta Learning分成两个阶段:

  1. Gradual Learning

    也就是通过在一些相似任务上训练得到Meta Learner,它具有较强的泛化能力。
  2. Rapid Learning

    也就是利用该Meta Learner直到之后的学习过程,使模型能够快速调整以适应新的任务。

在李宏毅的课程中提到,Machine Learning其实就是学习如何根据数据找出一个$f$函数,使得在测试中,该函数能根据输入获得正确的输出。而Meta Learning其实就是找到一个$F$函数,这个$F$函数的能力是找出$f$函数,也就是说,不同于机器学习直接找$f$,元学习中机器的任务是去找这个$F$。
来看下图,这里说的其实就是一个训练的过程,通过训练找出最终模型中效果最好的参数$\widehat{\theta }$。

可以看上图红框圈出的部分,仔细想想,这些部分的不同也就构成了不同的算法。然而,目前这些红框中的结构基本上还是人为设计定义的。那么元学习思考的就是能不能让机器自己学习其中的方法与结构。


数据集划分

由于元学习学习的是解决一系列任务的能力,因此不同于以往的将数据集划分成训练集和测试集(或许还有验证集),在元学习中,我们将数据集分割成Training Tasks、Testing Tasks(、Validaiton Tasks)。每一个任务都包含一个训练集和一个测试集。为了区别,这里称这些小训练集为Support Set,称这些小测试集为Query Set。


元学习与one-shot learning

一般来说,元学习总是与one-shot learning相结合的。我们知道,目前许多单任务模型的训练都是以天计数的。而元学习的训练集中每一组数据都是一个任务,假如Training Tasks中有n个任务,那就是说我们要训练n次也就是单任务模型的训练时间的n倍,这也意味着我们需要很长的时间才能检验一次效果,显然是不划算的。
而one-shot learning虽然只提供了少量的数据,但这也是它的一个优势——训练更快,因此我们对one-shot learning任务进行元学习,可以在较短的时间内看到训练效果。


学习初值的元学习

上文说过可以让机器自己学习红框中的结构与方法,这里有元学习中两种代表性的方法MAML和Reptile,它们都学习的是初始化的方式(上图第二个红框)。MAML音同mammal(哺乳动物),也是Model Agnostic Meta Learning的缩写;而Reptile(意为爬行动物),由于找不到缩写的来源,严重怀疑是故意这么命名来与MAML对应。
它们两者实际上学习的是该把初始化参数$\phi $设成什么,其损失函数为

这里的$\widehat{\theta }^{n}$其实就是在第n个任务中,由初始参数$\phi $通过Support Set学到的最终模型中的参数。而$l^{n}\left ( \widehat{\theta }^{n} \right )$就是对于第n个任务在Query Set上的损失。
首先我借用李宏毅老师给的例子来弄明白这里“学习把参数$\phi $初始化成什么”到底与普通的训练中“学习把参数训练成什么”有什么区别。

在上图中,左图表示的是我们一般意义上的训练(Model Pre-training),右图表示的是MAML或者Reptile中的初始化。可以看到,左图最终训练得出的参数$\phi $在task 1(Training Tasks)上表现得最好,但在一个不同的任务task 2(Testing Tasks)上,它只能收敛到局部最优点(local minima)。在右图中,虽然在Training Tasks上表现得不是最好,但是在遇到新的任务训练时,现在的初始化参数$\phi $可以很快很好地收敛到全局最优点(global minima)。
也就是说,一般意义上的训练,我们关注的是在所有Training Tasks上达到最好的情况,比如找出让训练时的损失函数最小的一组参数,但我们并不关注拿此时的参数去新任务上进行训练是否会得到好的结果;而在学习初始化的元学习中,我们不在意$\phi $在Training Tasks中表现得有多好,我们在意的是拿初始化后的$\phi $到下一个任务上去训练能够很快很好地取得不错的表现。简而言之,Model Pre-training关注的是$\phi $当前的表现,而Meta Learning关注的是$\phi $未来遇到新任务时的潜力。如果说到这里还不是很清楚,后文还有直观的理解。
具体怎么找到这个$\phi $呢?这就产生了下面两种方法。

  1. MAML模型无关元学习

    MAML考虑的是训练出一个初始化参数$\phi $,使得拥有这个参数的模型在遇到一个新的任务时能够一步到位达到最好,比如接收到一张新的图片就可以快速训练成为辨别该图片同一类别图片的最优模型。
    当然,寻找这个$\phi $的方法还是梯度下降,因此也要求$\phi $关于损失函数$L$的偏导数。由于MAML考虑的是模型在遇到新任务时一步到位,因此$\widehat{\theta }$也仅进行一次梯度下降,如下所示。 由于这里$\widehat{\theta }_{j}$对$\phi _{i}$的偏导数还是要分类讨论很麻烦,因此作者使用了a first-order approximation,如下图所示。 也就是当且仅当$i$等于$j$的时候时取$\widehat{\theta }_{j}$对$\phi _{i}$的偏导数为1,否则取偏导数为0。
    我们可以来直观地看一下MAML在做什么。如下图所示,左为MAML,右为Model Pre-training,上文已比较过两者关注点的不同,这里再看看两者方法上的不同。 MAML首先在第一个任务采样出来的样本上计算损失函数并进行一次梯度下降,然后用此时的参数计算损失函数再进行一次梯度下降,最后,根据第二次梯度下降的梯度方向,平行地对原参数$\phi $进行一次梯度下降。注意,这里每一次梯度下降使用的学习率不需要一样。可以这样理解,第二次梯度下降后的参数相比第一次梯度下降后的参数在task m上表现要好,因此$\phi $沿相同的方向梯度下降,就能使在遇到task m的时以更少的训练次数到达离较优的第二次梯度下降后的参数更近的位置。在task n上同理。
    而Model Pre-training,则是要求在训练的每个任务上尽可能做到最好,于是直接朝着由每个任务采样样本得到的损失函数计算出的梯度的方向靠近。
    其实,Model Pre-training的基本思想就是每训练完一个任务,我们就把这过程中的信息用来更新模型。而Meta Learning的基本思想就是找到所有学习任务背后的基础知识。因此,我们不会立即更新模型,而是等待一批任务完成。稍后,我们将从这些任务中学到的所有知识合并到一个更新中。 个人感觉,这有点像想要去做、却为了大局而保持一点克制的感觉,似乎的确蕴含了一点点人生哲理在里面。
  2. Reptile一阶元学习

    Reptile的思想其实是和MAML一样的,只不过它使用了多次梯度下降后的方向来更新$\phi _{i}$,也就是对遇到一个新任务时的更新次数不加限制,不指望一步到位。 我们可以通过Pre-training、MAML和Reptile的对比图来理解一下Reptile改进的思想,它相当于在当下的任务和未来的任务之间寻找一个平衡。 我觉得就泛化能力而言,MAML可能还是要好于Reptile的。
    实际上,在MAML的使用中,我们也可以进行多次梯度下降,训练时假设一步到位并不意味着在实际使用的时候只能使用一次梯度下降。

李宏毅老师在课程中还给出了一个toy example的例子,区分Model Pre-training和Meta Learning。如下所示,toy example的目标是拟合正弦曲线

这里的$a$和$b$是任取的随机数,也就是每个任务都是拟合一个不同的正弦曲线。
来看看两种方法的训练结果,其中左边是Model Pre-training的结果,右边是Meta Learning的结果,这里使用的是MAML方法。

图中绿色的线就是训练好之后的参数构成的模型。可以看到,Model Pre-training为了在训练集中各种正弦曲线中达到损失函数最小,训练结果为一条直线,这在测试时效果很差;而MAML则预备在遇到一个新的曲线时能很快地去拟合,在测试时能很快地达到很好的效果。
然而,目前这类方法的效果好像仅在多样性较低的任务分布中进行实验得到了验证,在这些任务上做得好不代表在复杂程度不同、模式不同的任务上也可以有很好的表现。对于应用在高复杂度的任务上的效果可能还是要打个问号的,在这方面的研究还有很长的路要走。

补充:其实这两种方法后面还是有比较复杂的数学推导的,我在网上看到一个博客写得挺好,补充在这里MAML-模型无关元学习算法Reptile-一阶元学习算法


学习更新的学习

上一节介绍的是如何让机器自己学习出一个初始化的方法,前文圈出的红框中另有一项是Update,那么我们是否可以学习如何更新模型呢?

借鉴LSTM

之前没有看过LSTM,所以我想顺便也理一下LSTM的基本思路。

相比于RNN,LSTM从前一级获得的输入有两个(也可以看作将一个输入拆成两段),区别就在于$c$更新得更慢,$h$更新得更快。

这里各个参数表示的含义如下所示,$\sigma $表示的是某种函数。

每一块的输出和传递给下一级的$c$与$h$计算方法如下所示,这里表示的是点乘操作。

  1. 方法1

    一般而言,我们梯度下降使用的是如下公式:对比LSTM中$c$的的更新公式$c^{t}=z^{f}\odot c^{t-1}+z^{i}\odot z$,我们很容易发现两者的结构有相通之处,由此产生灵感,我们是否可以把这里的$z^{f}$和$z^{i}$也变成可以学习的呢。也就是输入$\theta ^{t-1}$和$-\bigtriangledown _{\theta }l$,不人为设定$z^{f}$和$z^{i}$,直接输出$\theta ^{t}$。
    于是就有了如下方法。 但是,由于$-\bigtriangledown _{\theta }l$的计算依赖于$\theta ^{t-1}$(上图虚线表示),这在反向传播时会比较复杂,因此为了简化,在反向传播时不考虑$-\bigtriangledown _{\theta }l$对$\theta ^{t-1}$的依赖。 注意,如果对所有的参数都设计这样一种学习如何更新的方式,那么计算量是非常大的,因此我们往往对所有参数采用同一套学习出来的更新方式。这是合理的,因为往往我们也会对所有参数设置同样的学习率。
    作者对此进行实验来看一看$z^{f}$和$z^{i}$的学习结果究竟如何。如下所示,可见$z^{f}$最后还是收敛到了1,而$z^{i}$也集中在1附近,但也的确学到了一些不一样的信息。 该实验可以表明,我们一般使用的梯度下降公式是比较合理的。不过这里元学习的方法并不是没有作用,后面会提到它的效果。
  2. 方法2

    为了利用更多以往的“记忆”,也可以设计一种两层的LSTM,如下图所示。 实际上,LSTM这种循环序列模型本身就有利用过往记忆的功能,我将在下一节做更多的介绍。
  3. 效果

    其实这些方法还是比较难实现的,这里来看一下利用LSTM来更新参数最后的效果。 可以看到,这种方法在各种优化算法面前还是有明显的优势的。这里有一个问题是作者进行的实验都是在MNIST数据集上进行的,比如说这里Meta Learning的Training Tasks是识别0,1,2,3…7,Testing Tasks是识别8,9。其实这些任务是同一类任务。作者也注意到了这一点,因此他在训练时用的是20units,测试时使用了40units;训练时只用了1个layer,测试时用了2个layer。以此来证明利用LSTM来更新参数这种方法在不同的模型中的能力。

    预测梯度

    既然Meta Learning的目的是实现快速学习,而快速学习的关键就是神经网络的梯度下降要准且快,那么是否可以让神经网络利用以往的任务学习如何预测梯度,这样面对新的任务,只要梯度预测得准,那么学习得就会更快。
    在Learning to Learn by Gradient Descent by Gradient Descent一文中(好好品味这个题目),作者提出了这样一种方法,即训练一个通用的神经网络来预测梯度,用一次二次方程的回归问题来训练,这种方法得到的神经网络优化算法比Adam、RMSProp还要好。

学习记忆的学习

元学习其实也可以利用以往的经验来学习,于是这又诞生了一系列模型。

记忆增强神经网络

一般的神经网络不具有记忆功能,输出的结果只基于当前的输入。而LSTM网络的出现则让网络有了记忆,它能够根据之前的输入给出当前的输出。但是,LSTM的记忆程度并不是那么理想,对于比较长的输入序列,LSTM的最终输出只与最后的几步输入有关,也就是long dependency问题,当然这个问题可以由注意力机制解决,然而还是不能从根本上解决长期记忆的问题,原因是由于LSTM是假设在时间序列上的输入输出:由t-1时刻得到t时刻的输出,然后再循环输入t时刻的结果得到t+1时刻的输出,这样势必会使处于前面序列的输入被淹没,导致这部分记忆被“丢掉”。
基于以上问题,研究者们提出了神经图灵机等记忆增强神经网络模型。我们知道,人除了用脑子记,还会使用做笔记等方式做辅助,当我们忘记时,我们可以去看笔记来获得相关的记忆。记忆增强神经网络的思想就是如此,它让所有的笔记的“读”“写”操作都可微分化,因此可以用神经网络误差后向传播的方式去训练模型。
在记忆增强神经网络中,我们引入外部存储器来存储样本表示和类标签信息。以Meta-Learning with Memory-augmented Neural Networks这篇文章为例,我们看一下它的网络结构。

我们可以看到,网络的输入把上一次的y label也作为输入,并且添加了external memory存储上一次的x输入,这使得下一次输入后进行反向传播时,可以让y label和x建立联系,使得之后的x能够通过外部记忆获取相关图像进行比对来实现更好的预测。

WaveNet

WaveNet的网络每次都利用了之前的数据,因此可以考虑WaveNet的方式来实现Meta Learning。

SNAIL

SNAIL意为蜗牛(为什么这么多动物名…),其实直接把论文标题缩写之后和SNAIL是不一样的,之所以命名成SNAIL可能是因为网络结构有点像蜗牛吧。

  1. 时序卷积

    时序卷积是通过在时间维度上膨胀的一维卷积生成时序数据的结构,如下图所示。 这种时序卷积是因果的,所以在下一个时间节点生成的值只会被之前时间节点的信息影响,而不受未来信息的影响。相比较于传统的RNN,它提供了一种更直接,更高带宽的方式来获取过去的信息。但是,为了处理更长的序列,膨胀率通常是指数级增长的,所以需要的卷积层数和序列长度呈对数关系。因此,只能对很久之前的信息进行粗略的访问,有限的容量和位置依赖性对于元学习方法是不利的,不能充分利用大量的先前的经验。
  2. 注意力模块

    注意力模块可以让模型在可能的无限大的上下文中精确的定位信息,把上下文信息当做无序的键值对,通过内容对其进行查找。
  3. 网络

    SNAIL由两个时序卷积和attention交错组成。时序卷积可以在有限的上下文中提供高带宽的访问方式,attention可以在很大的上下文中精确地访问信息,所以将二者结合起来就得到了SNAIL。在时序卷积产生的上下文中应用causal attention,可以使网络学习应该挑选聚集哪些信息,以及如何更好地表示这些信息。

    注意:英语小贴士,因果:causal;休闲:casual。


学习方法的学习

元学习还用于对数据处理中的一些方法进行学习,比如这里要介绍的对特征提取方法的学习。

Siamese Network孪生网络

Siamese Network中文译为孪生网络。常用于人脸验证(不同于人脸识别),近几年在目标跟踪领域也大放光彩。
在Siamese Network中,我们首先使用两个共享模型参数值的相同网络来提取两个样本的特征(也有个别伪Siamese Network,它们不共享网络权重)。然后,我们将提取出的特征输入鉴别器,判断两个样本是否属于同一类对象。例如,我们可以计算其特征向量的余弦相似度(p)。如果它们相似,那么p应该接近1;否则,它们应该接近0。根据样本的标签和p,我们对网络进行相应的训练。简而言之,我们希望找到使样例属于同一类或将它们区分开来的特性。

这里利用神经网络的好处是它能够学习到哪些特征是该提取的,比如在一些传统的方法中,它们会对整个图像进行特征转化,其中也将包括背景,这样就可能会导致当两个相似背景但不同对象的图片作为输入时,输出会判定为相同。而神经网络它可以在训练过程中学习到哪些部分是值得关注的,可以学会ignore背景。
李宏毅老师在讲这一块的时候还提到了一种思路,就是学习一种算法(生成网络),这种算法能够使用one-shot learning输入的少量样本生成许多样本用于和prediction的那个输入进行比对验证,其框架与Siamese Network基本相似。

Matching Network匹配网络

Matching Network与Siamese Network十分相似。我们知道,人的注意力是可以利用以往的经验来实现提升的。那么,能不能利用以往的任务来训练一个attention模型,使得模型面对新的任务时,能够直接关注最重要的部分。
于是就有了Matching Network。

这里的g和f是特征提取器,和Siamese Network一样使用深度网络来提取特征,用于我们的输入和测试样本。通常情况下,g和f是相同的,共享相同的深度网络。然后,我们比较它们的相似度。同样,我们从预测中计算一个损失函数来训练我们的特征提取器。
这里构造了一个attention机制,也就是最后的label判断是通过attention的叠加得到的:

其实这里的网络结构远比上面画出来的复杂(用到了LSTM)。我们可以来看一下它的表现,在Omniglot数据集(找一批人照着象形文字画出来的)上,它取得了比Siamese Network更好一点的成绩。不过具体能力如何,还得看论文跑实验。

Prototypical Network原型网络

该方法思想十分简单,效果也非常好。它学习的是一个度量空间,通过计算和每个类别的原型表达的距离来进行分类。
具体的思路是:每个类别都存在一个聚在某单个原型表达周围的embedding,该类的原型是Support Set在embedding空间中的均值。然后,分类问题变成在embedding空间中的最近邻。如图所示,c1、c2、c3分别是三个类别的均值中心(称Prototype),将测试样本x进行embedding后,与这3个中心进行距离计算,从而获得x的类别。


学习损失的学习

前面提到,Meta Learning处理的主要是one-shot learning,而one-shot learning需要让学习的速度更快。学习效率除了用更好的梯度来提高,也可以考虑寻找更好的损失函数。根据这种思路,或许可以构造一个模型利用以往的任务来学习如何预测loss。

如上图所示,作者构造了一个Meta-Critic Network,其作用是用来学习如何预测Actor Network的loss。这也是一种思路吧。


学习结构的学习

还有一种思路就是结合深度学习动态地生成一个新的模型。

这种方法能使得模型更加准确,但不一定能够更有效地使用较少的样本来学习,不适合用于one-shot learning。


小结

套娃预警!
在古印度神话中,有人问世界在哪?答案是在乌龟的背上。那么乌龟在哪?乌龟在另一个大乌龟的背上。那么大乌龟在哪…

那么,我们之前让机器learn,现在想办法让机器learn to learn,那之后是不是还要learn to learn to learn再learn to learn to learn to learn呢?哈哈套娃警告。
难以想象未来的机器会是怎么样的,如此智能真的有点细思极恐呢。话说回来,Meta Learning方兴未艾,各种神奇的idea层出不穷,但是真正的杀手级算法尚未出现,让我们拭目以待!


碰到底线咯 后面没有啦

本文标题:deep learning笔记:元学习

文章作者:高深远

发布时间:2020年02月03日 - 16:22

最后更新:2020年03月27日 - 11:14

原始链接:https://gsy00517.github.io/deep-learning20200203162245/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:近年来深度学习的重要研究成果 | 高深远的博客

deep learning笔记:近年来深度学习的重要研究成果

今天在公众号上看到一篇综述论文的翻译,该论文列举出了近年来深度学习的一些重要研究成果,从方法、架构,以及正则化、优化技术方面进行概述。看完之后感觉对目前学术图景有了一次基本的认识,于是决定搬过来,对之后参考文献查找也能起到很大的帮助。
之前也分享过一篇deep-learning笔记:一篇非常经典的论文——NatureDeepReview,可以一起看一下。
觉得阅读英文版更好,或者看完后想寻找对应参考文献的读者可以直接去论文地址下载原文。


Abstract

深度学习是机器学习和人工智能研究的最新趋势之一。它也是当今最流行的科学研究趋势之一。深度学习方法为计算机视觉和机器学习带来了革命性的进步。新的深度学习技术正在不断诞生,超越最先进的机器学习甚至是现有的深度学习技术。近年来,全世界在这一领域取得了许多重大突破。由于深度学习正快度发展,导致了它的进展很难被跟进,特别是对于新的研究者。在本文中,我们将简要讨论近年来关于深度学习的最新进展。


Introduction

“深度学习”(DL)一词最初在1986年被引入机器学习(ML),后来在2000年时被用于人工神经网络(ANN)。深度学习方法由多个层组成,以学习具有多个抽象层次的数据特征。DL方法允许计算机通过相对简单的概念来学习复杂的概念。对于人工神经网络(ANN),深度学习(DL)(也称为分层学习(Hierarchical Learning))是指在多个计算阶段中精确地分配信用,以转换网络中的聚合激活。为了学习复杂的功能,深度架构被用于多个抽象层次,即非线性操作;例如 ANNs,具有许多隐藏层。用准确的话总结就是,深度学习是机器学习的一个子领域,它使用了多层次的非线性信息处理和抽象,用于有监督或无监督的特征学习、表示、分类和模式识别。
深度学习即表征学习是机器学习的一个分支或子领域,大多数人认为近代深度学习方法是从2006年开始发展起来的。本文是关于最新的深度学习技术的综述,主要推荐给即将涉足该领域的研究者。本文包括DL的基本思想、主要方法、最新进展以及应用。
综述论文是非常有益的,特别是对某一特定领域的新研究人员。一个研究领域如果在不久的将来及相关应用领域中有很大的价值,那通常很难被实时跟踪到最新进展。现在,科学研究是一个很有吸引力的职业,因为知识和教育比以往任何时候都更容易分享和获得。对于一种技术研究的趋势来说,唯一正常的假设是它会在各个方面有很多的改进。几年前对某个领域的概述,现在可能已经过时了。
考虑到近年来深度学习的普及和推广,我们简要概述了深度学习和神经网络(NN),以及它的主要进展和几年来的重大突破。我们希望这篇文章将帮助许多新手研究者在这一领域全面了解最近的深度学习的研究和技术,并引导他们以正确的方式开始。同时,我们希望通过这项工作,向这个时代的顶级DL和ANN研究者们致敬:Geoffrey Hinton(Hinton)、Juergen Schmidhuber(Schmidhuber)、Yann LeCun(LeCun)、Yoshua Bengio(Bengio)和许多其他研究学者,他们的研究构建了现代人工智能(AI)。跟进他们的工作,以追踪当前最佳的DL和ML研究进展对我们来说也至关重要。
在本论文中,我们首先简述过去的研究论文,对深度学习的模型和方法进行研究。然后,我们将开始描述这一领域的最新进展。我们将讨论深度学习(DL)方法、深度架构(即深度神经网络(DNN))和深度生成模型(DGM),其次是重要的正则化和优化方法。此外,用两个简短的部分对于开源的 DL 框架和重要的 DL 应用进行总结。我们将在最后两个章节(即讨论和结论)中讨论深入学习的现状和未来。


Related works

在过去的几年中,有许多关于深度学习的综述论文。他们以很好的方式描述了DL方法、方法论以及它们的应用和未来研究方向。这里,我们简要介绍一些关于深度学习的优秀综述论文。
Young等人(2017)讨论了DL模型和架构,主要用于自然语言处理(NLP)。他们在不同的NLP领域中展示了DL应用,比较了DL模型,并讨论了可能的未来趋势。
Zhang等人(2017)讨论了用于前端和后端语音识别系统的当前最佳深度学习技术。
Zhu等人(2017)综述了DL遥感技术的最新进展。他们还讨论了开源的DL框架和其他深度学习的技术细节。
Wang等人(2017)以时间顺序的方式描述了深度学习模型的演变。该短文简要介绍了模型,以及在DL研究中的突破。该文以进化的方式来了解深度学习的起源,并对神经网络的优化和未来的研究做了解读。
Goodfellow等人(2016)详细讨论了深度网络和生成模型,从机器学习(ML)基础知识、深度架构的优缺点出发,对近年来的DL研究和应用进行了总结。
LeCun等人(2015)从卷积神经网络(CNN)和递归神经网络(RNN)概述了深度学习(DL)模型。他们从表征学习的角度描述了DL,展示了DL技术如何工作、如何在各种应用中成功使用、以及如何对预测未来进行基于无监督学习(UL)的学习。同时他们还指出了DL在文献目录中的主要进展。
Schmidhuber(2015)从CNN、RNN和深度强化学习(RL)对深度学习做了一个概述。他强调了序列处理的RNN,同时指出基本DL和NN的局限性,以及改进它们的技巧。
Nielsen(2015)用代码和例子描述了神经网络的细节。他还在一定程度上讨论了深度神经网络和深度学习。
Schmidhuber(2014)讨论了基于时间序列的神经网络、采用机器学习方法进行分类,以及在神经网络中使用深度学习的历史和进展。
Deng和Yu(2014)描述了深度学习类别和技术,以及DL在几个领域的应用。
Bengio(2013)从表征学习的角度简要概述了DL算法,即监督和无监督网络、优化和训练模型。他聚焦于深度学习的许多挑战,例如:为更大的模型和数据扩展算法,减少优化困难,设计有效的缩放方法等。
Bengio等人(2013)讨论了表征和特征学习即深度学习。他们从应用、技术和挑战的角度探讨了各种方法和模型。
Deng(2011)从信息处理及相关领域的角度对深度结构化学习及其架构进行了概述。
Arel等人(2010)简要概述了近年来的DL技术。
Bengio(2009)讨论了深度架构,即人工智能的神经网络和生成模型。
最近所有关于深度学习(DL)的论文都从多个角度讨论了深度学习重点。这对DL的研究人员来说是非常有必要的。然而,DL目前是一个蓬勃发展的领域。在最近的DL概述论文发表之后,仍有许多新的技术和架构被提出。此外,以往的论文从不同的角度进行研究。我们的论文主要是针对刚进入这一领域的学习者和新手。为此,我们将努力为新研究人员和任何对这一领域感兴趣的人提供一个深度学习的基础和清晰的概念。


Recent Advances

在本节中,我们将讨论最近从机器学习和人工神经网络(ANN)的中衍生出来的主要深度学习(DL)方法,人工神经网络是深度学习最常用的形式。

Evolution of Deep Architectures

人工神经网络(ANN)已经取得了长足的进步,同时也带来了其他的深度模型。第一代人工神经网络由简单的感知器神经层组成,只能进行有限的简单计算。第二代使用反向传播,根据错误率更新神经元的权重。然后支持向量机(SVM)浮出水面,在一段时间内超越 ANN。为了克服反向传播的局限性,人们提出了受限玻尔兹曼机(RBM),使学习更容易。此时其他技术和神经网络也出现了,如前馈神经网络(FNN)、卷积神经网络(CNN)、循环神经网络(RNN)等,以及深层信念网络、自编码器等。从那时起,为实现各种用途,ANN在不同方面得到了改进和设计。
Schmidhuber(2014)、Bengio(2009)、Deng和Yu(2014)、Goodfellow等人(2016)、Wang等人(2017)对深度神经网络(DNN)的进化和历史以及深度学习(DL)进行了详细的概述。在大多数情况下,深层架构是简单架构的多层非线性重复,这样可从输入中获得高度复杂的函数。


Deep Learning Approaches

深度神经网络在监督学习中取得了巨大的成功。此外,深度学习模型在无监督、混合和强化学习方面也非常成功。

Deep Supervised Learning

监督学习应用在当数据标记、分类器分类或数值预测的情况。LeCun等人(2015)对监督学习方法以及深层结构的形成给出了一个精简的解释。Deng和Yu(2014)提到了许多用于监督和混合学习的深度网络,并做出解释,例如深度堆栈网络(DSN)及其变体。Schmidthuber(2014)的研究涵盖了所有神经网络,从早期神经网络到最近成功的卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆(LSTM)及其改进。

Deep Unsupervised Learning

当输入数据没有标记时,可应用无监督学习方法从数据中提取特征并对其进行分类或标记。LeCun等人(2015)预测了无监督学习在深度学习中的未来。Schmidthuber(2014)也描述了无监督学习的神经网络。Deng和Yu(2014)简要介绍了无监督学习的深度架构,并详细解释了深度自编码器。

Deep Reinforcement Learning

强化学习使用奖惩系统预测学习模型的下一步。这主要用于游戏和机器人,解决平常的决策问题。Schmidthuber(2014)描述了强化学习(RL)中深度学习的进展,以及深度前馈神经网络(FNN)和循环神经网络(RNN)在RL中的应用。Li(2017)讨论了深度强化学习(Deep Reinforcement Learning,DRL)、它的架构(例如Deep Q-Network,DQN)以及在各个领域的应用。
Mnih等人(2016)提出了一种利用异步梯度下降进行DNN优化的DRL框架。
van Hasselt等人(2015)提出了一种使用深度神经网络(deep neural network,DNN)的DRL架构。


Deep Neural Networks

在本节中,我们将简要地讨论深度神经网络(DNN),以及它们最近的改进和突破。神经网络的功能与人脑相似。它们主要由神经元和连接组成。当我们说深度神经网络时,我们可以假设有相当多的隐藏层,可以用来从输入中提取特征和计算复杂的函数。Bengio(2009)解释了深度结构的神经网络,如卷积神经网络(CNN)、自编码器(AE)等及其变体。Deng和Yu(2014)详细介绍了一些神经网络架构,如AE及其变体。Goodfellow等(2016)对深度前馈网络、卷积网络、递归网络及其改进进行了介绍和技巧性讲解。Schmidhuber(2014)提到了神经网络从早期神经网络到最近成功技术的完整历史。

Deep Autoencoders

自编码器(AE)是神经网络(NN),其中输出即输入。AE采用原始输入,编码为压缩表示,然后解码以重建输入。在深度AE中,低隐藏层用于编码,高隐藏层用于解码,误差反向传播用于训练。

Variational Autoencoders

变分自动编码器(VAE)可以算作解码器。VAE建立在标准神经网络上,可以通过随机梯度下降训练(Doersch,2016)。

Stacked Denoising Autoencoders

在早期的自编码器(AE)中,编码层的维度比输入层小(窄)。在多层降噪自编码器(SDAE)中,编码层比输入层宽(Deng and Yu,2014)。

Transforming Autoencoders

深度自动编码器(DAE)可以是转换可变的,也就是从多层非线性处理中提取的特征可以根据学习者的需要而改变。变换自编码器(TAE)既可以使用输入向量,也可以使用目标输出向量来应用转换不变性属性,将代码引导到期望的方向(Deng and Yu,2014)。

Deep Convolutional Neural Networks

四种基本思想构成了卷积神经网络(CNN),即:局部连接、共享权重、池化和多层使用。CNN的第一部分由卷积层和池化层组成,后一部分主要是全连接层。卷积层检测特征的局部连接,池层将相似的特征合并为一个。CNN在卷积层中使用卷积而不是矩阵乘法。
Krizhevsky等人(2012)提出了一种深度卷积神经网络(CNN)架构,也称为AlexNet,这是深度学习(Deep Learning,DL)的一个重大突破。网络由5个卷积层和3个全连接层组成。该架构采用图形处理单元(GPU)进行卷积运算,采用线性整流函数(ReLU)作为激活函数,用Dropout来减少过拟合。
Iandola等人(2016)提出了一个小型的CNN架构,叫做SqueezeNet。
Szegedy等人(2014)提出了一种深度CNN架构,名为Inception。Dai等人(2017)提出了对Inception-ResNet的改进。
Redmon等人(2015)提出了一个名为YOLO(You Only Look Once)的CNN架构,用于均匀和实时的目标检测。
Zeiler和Fergus(2013)提出了一种将CNN内部激活可视化的方法。
Gehring等人(2017)提出了一种用于序列到序列学习的CNN架构。
Bansal等人(2017)提出了PixelNet,使用像素来表示。
Goodfellow等人(2016)解释了CNN的基本架构和思想。Gu等人(2015)对CNN的最新进展、CNN的多种变体、CNN的架构、正则化方法和功能以及在各个领域的应用进行了很好的概述。

Deep Max-Pooling Convolutional Neural Networks

最大池化卷积神经网络(MPCNN)主要对卷积和最大池化进行操作,特别是在数字图像处理中。MPCNN通常由输入层以外的三种层组成。卷积层获取输入图像并生成特征图,然后应用非线性激活函数。最大池层向下采样图像,并保持子区域的最大值。全连接层进行线性乘法。在深度MPCNN中,在输入层之后周期性地使用卷积和混合池化,然后是全连接层。

Very Deep Convolutional Neural Networks

Simonyan和Zisserman(2014)提出了非常深层的卷积神经网络(VDCNN)架构,也称为VGG Net。VGG Net使用非常小的卷积滤波器,深度达到 16-19层。Conneau等人(2016)提出了另一种文本分类的VDCNN架构,使用小卷积和池化。他们声称这个VDCNN架构是第一个在文本处理中使用的,它在字符级别上起作用。该架构由29个卷积层组成。

Network In Network

Lin等人(2013)提出了网络中的网络(Network In Network,NIN)。NIN以具有复杂结构的微神经网络代替传统卷积神经网络(CNN)的卷积层。它使用多层感知器(MLPConv)处理微神经网络和全局平均池化层,而不是全连接层。深度NIN架构可以由NIN结构的多重叠加组成。

Region-based Convolutional Neural Networks

Girshick等人(2014)提出了基于区域的卷积神经网络(R-CNN),使用区域进行识别。R-CNN使用区域来定位和分割目标。该架构由三个模块组成:定义了候选区域的集合的类别独立区域建议,从区域中提取特征的大型卷积神经网络(CNN),以及一组类特定的线性支持向量机(SVM)。

Fast R-CNN

Girshick(2015)提出了快速的基于区域的卷积网络(Fast R-CNN)。这种方法利用R-CNN架构能快速地生成结果。Fast R-CNN由卷积层和池化层、区域建议层和一系列全连接层组成。

Faster R-CNN

Ren等人(2015)提出了更快的基于区域的卷积神经网络(Faster R-CNN),它使用区域建议网络(Region Proposal Network,RPN)进行实时目标检测。RPN是一个全卷积网络,能够准确、高效地生成区域建议(Ren et al.,2015)。

Mask R-CNN

何恺明等人(2017)提出了基于区域的掩模卷积网络(Mask R-CNN)实例目标分割。Mask R-CNN扩展了R-CNN的架构,并使用一个额外的分支用于预测目标掩模。

Multi-Expert R-CNN

Lee等人(2017)提出了基于区域的多专家卷积神经网络(ME R-CNN),利用了Fast R-CNN架构。ME R-CNN从选择性和详尽的搜索中生成兴趣区域(RoI)。它也使用per-RoI多专家网络而不是单一的per-RoI网络。每个专家都是来自Fast R-CNN的全连接层的相同架构。

Deep Residual Networks

He等人(2015)提出的残差网络(ResNet)由152层组成。ResNet具有较低的误差,并且容易通过残差学习进行训练。更深层次的ResNet可以获得更好的性能。在深度学习领域,人们认为ResNet是一个重要的进步。

Resnet in Resnet

Targ等人(2016)在Resnet in Resnet(RiR)中提出将ResNets和标准卷积神经网络(CNN)结合到深层双流架构中。

ResNeXt

Xie等人(2016)提出了ResNeXt架构。ResNext利用ResNets来重复使用分割-转换-合并策略。

Capsule Networks

Sabour等人(2017)提出了胶囊网络(CapsNet),即一个包含两个卷积层和一个全连接层的架构。CapsNet通常包含多个卷积层,胶囊层位于末端。CapsNet被认为是深度学习的最新突破之一,因为据说这是基于卷积神经网络的局限性而提出的。它使用的是一层又一层的胶囊,而不是神经元。激活的较低级胶囊做出预测,在同意多个预测后,更高级的胶囊变得活跃。在这些胶囊层中使用了一种协议路由机制。Hinton之后提出EM路由,利用期望最大化(EM)算法对CapsNet进行了改进。

Recurrent Neural Networks

循环神经网络(RNN)更适合于序列输入,如语音、文本和生成序列。一个重复的隐藏单元在时间展开时可以被认为是具有相同权重的非常深的前馈网络。由于梯度消失和维度爆炸问题,RNN曾经很难训练。为了解决这个问题,后来许多人提出了改进意见。
Goodfellow等人(2016)详细分析了循环和递归神经网络和架构的细节,以及相关的门控和记忆网络。
Karpathy等人(2015)使用字符级语言模型来分析和可视化预测、表征训练动态、RNN及其变体(如LSTM)的错误类型等。
J´ozefowicz等人(2016)探讨了RNN模型和语言模型的局限性。

RNN-EM

Peng和Yao(2015)提出了利用外部记忆(RNN-EM)来改善RNN的记忆能力。他们声称在语言理解方面达到了最先进的水平,比其他RNN更好。

GF-RNN

Chung等人(2015)提出了门控反馈递归神经网络(GF-RNN),它通过将多个递归层与全局门控单元叠加来扩展标准的RNN。

CRF-RNN

Zheng等人(2015)提出条件随机场作为循环神经网络(CRF-RNN),其将卷积神经网络(CNN)和条件随机场(CRF)结合起来进行概率图形建模。

Quasi-RNN

Bradbury等人(2016)提出了用于神经序列建模和沿时间步的并行应用的准循环神经网络(QRNN)。

Memory Networks

Weston等人(2014)提出了问答记忆网络(QA)。记忆网络由记忆、输入特征映射、泛化、输出特征映射和响应组成。

Dynamic Memory Networks

Kumar等人(2015)提出了用于QA任务的动态记忆网络(DMN)。DMN有四个模块:输入、问题、情景记忆、输出。

Augmented Neural Networks

Olah和Carter(2016)很好地展示了注意力和增强循环神经网络,即神经图灵机(NTM)、注意力接口、神经编码器和自适应计算时间。增强神经网络通常是使用额外的属性,如逻辑函数以及标准的神经网络架构。

Neural Turing Machines

Graves等人(2014)提出了神经图灵机(NTM)架构,由神经网络控制器和记忆库组成。NTM通常将RNN与外部记忆库结合。

Neural GPU

Kaiser和Sutskever(2015)提出了神经 GPU,解决了NTM的并行问题。

Neural Random-Access Machines

Kurach等人(2015)提出了神经随机存取机,它使用外部的可变大小的随机存取存储器。

Neural Programmer

Neelakantan等人(2015)提出了神经编程器,一种具有算术和逻辑功能的增强神经网络。

Neural Programmer-Interpreters

Reed和de Freitas(2015)提出了可以学习的神经编程器-解释器(NPI)。NPI包括周期性内核、程序内存和特定于领域的编码器。

Long Short Term Memory Networks

Hochreiter和Schmidhuber(1997)提出了长短期记忆(Long Short-Term Memory,LSTM),克服了循环神经网络(RNN)的误差回流问题。LSTM是基于循环网络和基于梯度的学习算法,LSTM引入自循环产生路径,使得梯度能够流动。
Greff等人(2017)对标准LSTM和8个LSTM变体进行了大规模分析,分别用于语音识别、手写识别和复调音乐建模。他们声称LSTM的8个变体没有显著改善,而只有标准LSTM表现良好。
Shi等人(2016)提出了深度长短期记忆网络(DLSTM),它是一个LSTM单元的堆栈,用于特征映射学习表示。

Batch-Normalized LSTM

Cooijmans等人(2016)提出了批归一化LSTM(BN-LSTM),它对递归神经网络的隐藏状态使用批归一化。

Pixel RNN

van den Oord等人(2016)提出像素递归神经网络(Pixel-RNN),由12个二维LSTM层组成。

Bidirectional LSTM

W¨ollmer等人(2010)提出了双向LSTM(BLSTM)的循环网络与动态贝叶斯网络(DBN)一起用于上下文敏感关键字检测。

Variational Bi-LSTM

Shabanian等人(2017)提出了变分双向LSTM(Variational Bi-LSTM),它是双向LSTM体系结构的变体。Variational Bi-LSTM使用变分自编码器(VAE)在LSTM之间创建一个信息交换通道,以学习更好的表征。

Googles Neural Machine Translation

Wu等人(2016)提出了名为谷歌神经机器翻译(GNMT)的自动翻译系统,该系统结合了编码器网络、解码器网络和注意力网络,遵循共同的序列对序列(sequence-to-sequence)的学习框架。

Fader Network

Lample等人(2017)提出了Fader网络,这是一种新型的编码器-解码器架构,通过改变属性值来生成真实的输入图像变化。

Hyper Networks

Ha等人(2016)提出的超网络(Hyper Networks)为其他神经网络生成权值,如静态超网络卷积网络、用于循环网络的动态超网络。
Deutsch(2018)使用超网络生成神经网络。

Highway Networks

Srivastava等人(2015)提出了高速路网络(Highway Networks),通过使用门控单元来学习管理信息。跨多个层次的信息流称为信息高速路。

Recurrent Highway Networks

Zilly等人(2017)提出了循环高速路网络(Recurrent Highway Networks,RHN),它扩展了长短期记忆(LSTM)架构。RHN在周期性过渡中使用了Highway层。

Highway LSTM RNN

Zhang等人(2016)提出了高速路长短期记忆Highway Long Short-Term Memory(HLSTM)RNN,它在相邻层的内存单元之间扩展了具有封闭方向连接(即Highway)的深度LSTM网络。

Long-Term Recurrent CNN

Donahue等人(2014)提出了长期循环卷积网络(LRCN),它使用CNN进行输入,然后使用LSTM进行递归序列建模并生成预测。

Deep Neural SVM

Zhang等人(2015)提出了深度神经SVM(DNSVM),它以支持向量机(Support Vector Machine,SVM)作为深度神经网络(Deep Neural Network,DNN)分类的顶层。

Convolutional Residual Memory Networks

Moniz和Pal(2016)提出了卷积残差记忆网络,将记忆机制并入卷积神经网络(CNN)。它用一个长短期记忆机制来增强卷积残差网络。

Fractal Networks

Larsson等人(2016)提出分形网络即FractalNet作为残差网络的替代方案。他们声称可以训练超深度的神经网络而不需要残差学习。分形是简单扩展规则生成的重复架构。

WaveNet

van den Oord等人(2016)提出了用于产生原始音频的深度神经网络WaveNet。WaveNet由一堆卷积层和softmax分布层组成,用于输出。
Rethage等人(2017)提出了一个WaveNet模型用于语音去噪。

Pointer Networks

Vinyals等人(2017)提出了指针网络(Ptr-Nets),通过使用一种称为“指针”的softmax概率分布来解决表征变量字典的问题。


Deep Generative Models

在本节中,我们将简要讨论其他深度架构,它们使用与深度神经网络类似的多个抽象层和表示层,也称为深度生成模型(deep generate Models,DGM)。Bengio(2009)解释了深层架构,例如Boltzmann machines(BM)和Restricted Boltzmann Machines(RBM)等及其变体。
Goodfellow等人(2016)详细解释了深度生成模型,如受限和非受限的玻尔兹曼机及其变种、深度玻尔兹曼机、深度信念网络(DBN)、定向生成网络和生成随机网络等。
Maaløe等人(2016)提出了辅助的深层生成模型(Auxiliary Deep Generative Models),在这些模型中,他们扩展了具有辅助变量的深层生成模型。辅助变量利用随机层和跳过连接生成变分分布。
Rezende等人(2016)开发了一种深度生成模型的单次泛化。

Boltzmann Machines

玻尔兹曼机是学习任意概率分布的连接主义方法,使用最大似然原则进行学习。

Restricted Boltzmann Machines

受限玻尔兹曼机(Restricted Boltzmann Machines,RBM)是马尔可夫随机场的一种特殊类型,包含一层随机隐藏单元,即潜变量和一层可观测变量。
Hinton和Salakhutdinov(2011)提出了一种利用受限玻尔兹曼机(RBM)进行文档处理的深度生成模型。

Deep Belief Networks

深度信念网络(Deep Belief Networks,DBN)是具有多个潜在二元或真实变量层的生成模型。
Ranzato等人(2011)利用深度信念网络(deep Belief Network,DBN)建立了深度生成模型进行图像识别。

Deep Lambertian Networks

Tang等人(2012)提出了深度朗伯网络(Deep Lambertian Networks,DLN),它是一个多层次的生成模型,其中潜在的变量是反照率、表面法线和光源。DLNis是朗伯反射率与高斯受限玻尔兹曼机和深度信念网络的结合。

Generative Adversarial Networks

Goodfellow等人(2014)提出了生成对抗网络(Generate Adversarial Nets,GAN),用于通过对抗过程来评估生成模型。GAN架构是由一个针对对手(即一个学习模型或数据分布的判别模型)的生成模型组成。Mao等人(2016)、Kim等人(2017)对GAN提出了更多的改进。
Salimans等人(2016)提出了几种训练GANs的方法。

Laplacian Generative Adversarial Networks

Denton等人(2015)提出了一种深度生成模型(DGM),叫做拉普拉斯生成对抗网络(LAPGAN),使用生成对抗网络(GAN)方法。该模型还在拉普拉斯金字塔框架中使用卷积网络。

Recurrent Support Vector Machines

Shi等人(2016)提出了循环支持向量机(RSVM),利用循环神经网络(RNN)从输入序列中提取特征,用标准支持向量机(SVM)进行序列级目标识别。


Training and Optimization Techniques

在本节中,我们将简要概述一些主要的技术,用于正则化和优化深度神经网络(DNN)。

Dropout

Srivastava等人(2014)提出Dropout,以防止神经网络过拟合。Dropout是一种神经网络模型平均正则化方法,通过增加噪声到其隐藏单元。在训练过程中,它会从神经网络中随机抽取出单元和连接。Dropout可以用于像RBM(Srivastava et al.,2014)这样的图形模型中,也可以用于任何类型的神经网络。最近提出的一个关于Dropout的改进是Fraternal Dropout,用于循环神经网络(RNN)。

Maxout

Goodfellow等人(2013)提出Maxout,一种新的激活函数,用于Dropout。Maxout的输出是一组输入的最大值,有利于Dropout的模型平均。

Zoneout

Krueger等人(2016)提出了循环神经网络(RNN)的正则化方法Zoneout。Zoneout在训练中随机使用噪音,类似于Dropout,但保留了隐藏的单元而不是丢弃。

Deep Residual Learning

He等人(2015)提出了深度残差学习框架,该框架被称为低训练误差的ResNet。

Batch Normalization

Ioffe和Szegedy(2015)提出了批归一化,通过减少内部协变量移位来加速深度神经网络训练的方法。Ioffe(2017)提出批重归一化,扩展了以前的方法。

Distillation

Hinton等人(2015)提出了将知识从高度正则化模型的集合(即神经网络)转化为压缩小模型的方法。

Layer Normalization

Ba等人(2016)提出了层归一化,特别是针对RNN的深度神经网络加速训练,解决了批归一化的局限性。


Deep Learning frameworks

有大量的开源库和框架可供深度学习使用。它们大多数是为Python编程语言构建的。如Theano、Tensorflow、PyTorch、PyBrain、Caffe、Blocks and Fuel、CuDNN、Honk、ChainerCV、PyLearn2、Chainer、torch等。


Applications of Deep Learning

在本节中,我们将简要地讨论一些最近在深度学习方面的杰出应用。自深度学习(DL)开始以来,DL方法以监督、非监督、半监督或强化学习的形式被广泛应用于各个领域。从分类和检测任务开始,DL应用正在迅速扩展到每一个领域。
例如:

  • 图像分类与识别
  • 视频分类
  • 序列生成
  • 缺陷分类
  • 文本、语音、图像和视频处理
  • 文本分类
  • 语音处理
  • 语音识别和口语理解
  • 文本到语音生成
  • 查询分类
  • 句子分类
  • 句子建模
  • 词汇处理
  • 预选择
  • 文档和句子处理
  • 生成图像文字说明
  • 照片风格迁移
  • 自然图像流形
  • 图像着色
  • 图像问答
  • 生成纹理和风格化图像
  • 视觉和文本问答
  • 视觉识别和描述
  • 目标识别
  • 文档处理
  • 人物动作合成和编辑
  • 歌曲合成
  • 身份识别
  • 人脸识别和验证
  • 视频动作识别
  • 人类动作识别
  • 动作识别
  • 分类和可视化动作捕捉序列
  • 手写生成和预测
  • 自动化和机器翻译
  • 命名实体识别
  • 移动视觉
  • 对话智能体
  • 调用遗传变异
  • 癌症检测
  • X射线CT重建
  • 癫痫发作预测
  • 硬件加速
  • 机器人

等。
Deng和Yu(2014)提供了DL在语音处理、信息检索、目标识别、计算机视觉、多模态、多任务学习等领域应用的详细列表。
使用深度强化学习(Deep Reinforcement Learning,DRL)来掌握游戏已经成为当今的一个热门话题。每到现在,人工智能机器人都是用DNN和DRL创建的,它们在战略和其他游戏中击败了人类世界冠军和象棋大师,从几个小时的训练开始。例如围棋的AlphaGo和AlphaGo Zero。


Discussion

尽管深度学习在许多领域取得了巨大的成功,但它还有很长的路要走。还有很多地方有待改进。至于局限性,例子也是相当多的。例如:Nguyen等人表明深度神经网络(DNN)在识别图像时容易被欺骗。还有其他问题,如Yosinski等人提出的学习的特征可迁移性。Huang等人提出了一种神经网络攻击防御的体系结构,认为未来的工作需要防御这些攻击。Zhang等人则提出了一个理解深度学习模型的实验框架,他们认为理解深度学习需要重新思考和概括。
Marcus在2018年对深度学习(Deep Learning,DL)的作用、局限性和本质进行了重要的回顾。他强烈指出了DL方法的局限性,即需要更多的数据,容量有限,不能处理层次结构,无法进行开放式推理,不能充分透明,不能与先验知识集成,不能区分因果关系。他还提到,DL假设了一个稳定的世界,以近似方法实现,工程化很困难,并且存在着过度炒作的潜在风险。Marcus认为DL需要重新概念化,并在非监督学习、符号操作和混合模型中寻找可能性,从认知科学和心理学中获得见解,并迎接更大胆的挑战。


Conclusion

尽管深度学习(DL)比以往任何时候都更快地推进了世界的发展,但仍有许多方面值得我们去研究。我们仍然无法完全地理解深度学习,我们如何让机器变得更聪明,更接近或比人类更聪明,或者像人类一样学习。DL一直在解决许多问题,同时将技术应用到方方面面。但是人类仍然面临着许多难题,例如仍有人死于饥饿和粮食危机,癌症和其他致命的疾病等。我们希望深度学习和人工智能将更加致力于改善人类的生活质量,通过开展最困难的科学研究。最后但也是最重要的,愿我们的世界变得更加美好。


碰到底线咯 后面没有啦

本文标题:deep learning笔记:近年来深度学习的重要研究成果

文章作者:高深远

发布时间:2020年02月15日 - 07:19

最后更新:2020年02月16日 - 07:14

原始链接:https://gsy00517.github.io/deep-learning20200215071915/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
deep learning笔记:近年来深度学习的重要研究成果 | 高深远的博客

deep learning笔记:近年来深度学习的重要研究成果

今天在公众号上看到一篇综述论文的翻译,该论文列举出了近年来深度学习的一些重要研究成果,从方法、架构,以及正则化、优化技术方面进行概述。看完之后感觉对目前学术图景有了一次基本的认识,于是决定搬过来,对之后参考文献查找也能起到很大的帮助。
之前也分享过一篇deep-learning笔记:一篇非常经典的论文——NatureDeepReview,可以一起看一下。
觉得阅读英文版更好,或者看完后想寻找对应参考文献的读者可以直接去论文地址下载原文。


Abstract

深度学习是机器学习和人工智能研究的最新趋势之一。它也是当今最流行的科学研究趋势之一。深度学习方法为计算机视觉和机器学习带来了革命性的进步。新的深度学习技术正在不断诞生,超越最先进的机器学习甚至是现有的深度学习技术。近年来,全世界在这一领域取得了许多重大突破。由于深度学习正快度发展,导致了它的进展很难被跟进,特别是对于新的研究者。在本文中,我们将简要讨论近年来关于深度学习的最新进展。


Introduction

“深度学习”(DL)一词最初在1986年被引入机器学习(ML),后来在2000年时被用于人工神经网络(ANN)。深度学习方法由多个层组成,以学习具有多个抽象层次的数据特征。DL方法允许计算机通过相对简单的概念来学习复杂的概念。对于人工神经网络(ANN),深度学习(DL)(也称为分层学习(Hierarchical Learning))是指在多个计算阶段中精确地分配信用,以转换网络中的聚合激活。为了学习复杂的功能,深度架构被用于多个抽象层次,即非线性操作;例如 ANNs,具有许多隐藏层。用准确的话总结就是,深度学习是机器学习的一个子领域,它使用了多层次的非线性信息处理和抽象,用于有监督或无监督的特征学习、表示、分类和模式识别。
深度学习即表征学习是机器学习的一个分支或子领域,大多数人认为近代深度学习方法是从2006年开始发展起来的。本文是关于最新的深度学习技术的综述,主要推荐给即将涉足该领域的研究者。本文包括DL的基本思想、主要方法、最新进展以及应用。
综述论文是非常有益的,特别是对某一特定领域的新研究人员。一个研究领域如果在不久的将来及相关应用领域中有很大的价值,那通常很难被实时跟踪到最新进展。现在,科学研究是一个很有吸引力的职业,因为知识和教育比以往任何时候都更容易分享和获得。对于一种技术研究的趋势来说,唯一正常的假设是它会在各个方面有很多的改进。几年前对某个领域的概述,现在可能已经过时了。
考虑到近年来深度学习的普及和推广,我们简要概述了深度学习和神经网络(NN),以及它的主要进展和几年来的重大突破。我们希望这篇文章将帮助许多新手研究者在这一领域全面了解最近的深度学习的研究和技术,并引导他们以正确的方式开始。同时,我们希望通过这项工作,向这个时代的顶级DL和ANN研究者们致敬:Geoffrey Hinton(Hinton)、Juergen Schmidhuber(Schmidhuber)、Yann LeCun(LeCun)、Yoshua Bengio(Bengio)和许多其他研究学者,他们的研究构建了现代人工智能(AI)。跟进他们的工作,以追踪当前最佳的DL和ML研究进展对我们来说也至关重要。
在本论文中,我们首先简述过去的研究论文,对深度学习的模型和方法进行研究。然后,我们将开始描述这一领域的最新进展。我们将讨论深度学习(DL)方法、深度架构(即深度神经网络(DNN))和深度生成模型(DGM),其次是重要的正则化和优化方法。此外,用两个简短的部分对于开源的 DL 框架和重要的 DL 应用进行总结。我们将在最后两个章节(即讨论和结论)中讨论深入学习的现状和未来。


Related works

在过去的几年中,有许多关于深度学习的综述论文。他们以很好的方式描述了DL方法、方法论以及它们的应用和未来研究方向。这里,我们简要介绍一些关于深度学习的优秀综述论文。
Young等人(2017)讨论了DL模型和架构,主要用于自然语言处理(NLP)。他们在不同的NLP领域中展示了DL应用,比较了DL模型,并讨论了可能的未来趋势。
Zhang等人(2017)讨论了用于前端和后端语音识别系统的当前最佳深度学习技术。
Zhu等人(2017)综述了DL遥感技术的最新进展。他们还讨论了开源的DL框架和其他深度学习的技术细节。
Wang等人(2017)以时间顺序的方式描述了深度学习模型的演变。该短文简要介绍了模型,以及在DL研究中的突破。该文以进化的方式来了解深度学习的起源,并对神经网络的优化和未来的研究做了解读。
Goodfellow等人(2016)详细讨论了深度网络和生成模型,从机器学习(ML)基础知识、深度架构的优缺点出发,对近年来的DL研究和应用进行了总结。
LeCun等人(2015)从卷积神经网络(CNN)和递归神经网络(RNN)概述了深度学习(DL)模型。他们从表征学习的角度描述了DL,展示了DL技术如何工作、如何在各种应用中成功使用、以及如何对预测未来进行基于无监督学习(UL)的学习。同时他们还指出了DL在文献目录中的主要进展。
Schmidhuber(2015)从CNN、RNN和深度强化学习(RL)对深度学习做了一个概述。他强调了序列处理的RNN,同时指出基本DL和NN的局限性,以及改进它们的技巧。
Nielsen(2015)用代码和例子描述了神经网络的细节。他还在一定程度上讨论了深度神经网络和深度学习。
Schmidhuber(2014)讨论了基于时间序列的神经网络、采用机器学习方法进行分类,以及在神经网络中使用深度学习的历史和进展。
Deng和Yu(2014)描述了深度学习类别和技术,以及DL在几个领域的应用。
Bengio(2013)从表征学习的角度简要概述了DL算法,即监督和无监督网络、优化和训练模型。他聚焦于深度学习的许多挑战,例如:为更大的模型和数据扩展算法,减少优化困难,设计有效的缩放方法等。
Bengio等人(2013)讨论了表征和特征学习即深度学习。他们从应用、技术和挑战的角度探讨了各种方法和模型。
Deng(2011)从信息处理及相关领域的角度对深度结构化学习及其架构进行了概述。
Arel等人(2010)简要概述了近年来的DL技术。
Bengio(2009)讨论了深度架构,即人工智能的神经网络和生成模型。
最近所有关于深度学习(DL)的论文都从多个角度讨论了深度学习重点。这对DL的研究人员来说是非常有必要的。然而,DL目前是一个蓬勃发展的领域。在最近的DL概述论文发表之后,仍有许多新的技术和架构被提出。此外,以往的论文从不同的角度进行研究。我们的论文主要是针对刚进入这一领域的学习者和新手。为此,我们将努力为新研究人员和任何对这一领域感兴趣的人提供一个深度学习的基础和清晰的概念。


Recent Advances

在本节中,我们将讨论最近从机器学习和人工神经网络(ANN)的中衍生出来的主要深度学习(DL)方法,人工神经网络是深度学习最常用的形式。

Evolution of Deep Architectures

人工神经网络(ANN)已经取得了长足的进步,同时也带来了其他的深度模型。第一代人工神经网络由简单的感知器神经层组成,只能进行有限的简单计算。第二代使用反向传播,根据错误率更新神经元的权重。然后支持向量机(SVM)浮出水面,在一段时间内超越 ANN。为了克服反向传播的局限性,人们提出了受限玻尔兹曼机(RBM),使学习更容易。此时其他技术和神经网络也出现了,如前馈神经网络(FNN)、卷积神经网络(CNN)、循环神经网络(RNN)等,以及深层信念网络、自编码器等。从那时起,为实现各种用途,ANN在不同方面得到了改进和设计。
Schmidhuber(2014)、Bengio(2009)、Deng和Yu(2014)、Goodfellow等人(2016)、Wang等人(2017)对深度神经网络(DNN)的进化和历史以及深度学习(DL)进行了详细的概述。在大多数情况下,深层架构是简单架构的多层非线性重复,这样可从输入中获得高度复杂的函数。


Deep Learning Approaches

深度神经网络在监督学习中取得了巨大的成功。此外,深度学习模型在无监督、混合和强化学习方面也非常成功。

Deep Supervised Learning

监督学习应用在当数据标记、分类器分类或数值预测的情况。LeCun等人(2015)对监督学习方法以及深层结构的形成给出了一个精简的解释。Deng和Yu(2014)提到了许多用于监督和混合学习的深度网络,并做出解释,例如深度堆栈网络(DSN)及其变体。Schmidthuber(2014)的研究涵盖了所有神经网络,从早期神经网络到最近成功的卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆(LSTM)及其改进。

Deep Unsupervised Learning

当输入数据没有标记时,可应用无监督学习方法从数据中提取特征并对其进行分类或标记。LeCun等人(2015)预测了无监督学习在深度学习中的未来。Schmidthuber(2014)也描述了无监督学习的神经网络。Deng和Yu(2014)简要介绍了无监督学习的深度架构,并详细解释了深度自编码器。

Deep Reinforcement Learning

强化学习使用奖惩系统预测学习模型的下一步。这主要用于游戏和机器人,解决平常的决策问题。Schmidthuber(2014)描述了强化学习(RL)中深度学习的进展,以及深度前馈神经网络(FNN)和循环神经网络(RNN)在RL中的应用。Li(2017)讨论了深度强化学习(Deep Reinforcement Learning,DRL)、它的架构(例如Deep Q-Network,DQN)以及在各个领域的应用。
Mnih等人(2016)提出了一种利用异步梯度下降进行DNN优化的DRL框架。
van Hasselt等人(2015)提出了一种使用深度神经网络(deep neural network,DNN)的DRL架构。


Deep Neural Networks

在本节中,我们将简要地讨论深度神经网络(DNN),以及它们最近的改进和突破。神经网络的功能与人脑相似。它们主要由神经元和连接组成。当我们说深度神经网络时,我们可以假设有相当多的隐藏层,可以用来从输入中提取特征和计算复杂的函数。Bengio(2009)解释了深度结构的神经网络,如卷积神经网络(CNN)、自编码器(AE)等及其变体。Deng和Yu(2014)详细介绍了一些神经网络架构,如AE及其变体。Goodfellow等(2016)对深度前馈网络、卷积网络、递归网络及其改进进行了介绍和技巧性讲解。Schmidhuber(2014)提到了神经网络从早期神经网络到最近成功技术的完整历史。

Deep Autoencoders

自编码器(AE)是神经网络(NN),其中输出即输入。AE采用原始输入,编码为压缩表示,然后解码以重建输入。在深度AE中,低隐藏层用于编码,高隐藏层用于解码,误差反向传播用于训练。

Variational Autoencoders

变分自动编码器(VAE)可以算作解码器。VAE建立在标准神经网络上,可以通过随机梯度下降训练(Doersch,2016)。

Stacked Denoising Autoencoders

在早期的自编码器(AE)中,编码层的维度比输入层小(窄)。在多层降噪自编码器(SDAE)中,编码层比输入层宽(Deng and Yu,2014)。

Transforming Autoencoders

深度自动编码器(DAE)可以是转换可变的,也就是从多层非线性处理中提取的特征可以根据学习者的需要而改变。变换自编码器(TAE)既可以使用输入向量,也可以使用目标输出向量来应用转换不变性属性,将代码引导到期望的方向(Deng and Yu,2014)。

Deep Convolutional Neural Networks

四种基本思想构成了卷积神经网络(CNN),即:局部连接、共享权重、池化和多层使用。CNN的第一部分由卷积层和池化层组成,后一部分主要是全连接层。卷积层检测特征的局部连接,池层将相似的特征合并为一个。CNN在卷积层中使用卷积而不是矩阵乘法。
Krizhevsky等人(2012)提出了一种深度卷积神经网络(CNN)架构,也称为AlexNet,这是深度学习(Deep Learning,DL)的一个重大突破。网络由5个卷积层和3个全连接层组成。该架构采用图形处理单元(GPU)进行卷积运算,采用线性整流函数(ReLU)作为激活函数,用Dropout来减少过拟合。
Iandola等人(2016)提出了一个小型的CNN架构,叫做SqueezeNet。
Szegedy等人(2014)提出了一种深度CNN架构,名为Inception。Dai等人(2017)提出了对Inception-ResNet的改进。
Redmon等人(2015)提出了一个名为YOLO(You Only Look Once)的CNN架构,用于均匀和实时的目标检测。
Zeiler和Fergus(2013)提出了一种将CNN内部激活可视化的方法。
Gehring等人(2017)提出了一种用于序列到序列学习的CNN架构。
Bansal等人(2017)提出了PixelNet,使用像素来表示。
Goodfellow等人(2016)解释了CNN的基本架构和思想。Gu等人(2015)对CNN的最新进展、CNN的多种变体、CNN的架构、正则化方法和功能以及在各个领域的应用进行了很好的概述。

Deep Max-Pooling Convolutional Neural Networks

最大池化卷积神经网络(MPCNN)主要对卷积和最大池化进行操作,特别是在数字图像处理中。MPCNN通常由输入层以外的三种层组成。卷积层获取输入图像并生成特征图,然后应用非线性激活函数。最大池层向下采样图像,并保持子区域的最大值。全连接层进行线性乘法。在深度MPCNN中,在输入层之后周期性地使用卷积和混合池化,然后是全连接层。

Very Deep Convolutional Neural Networks

Simonyan和Zisserman(2014)提出了非常深层的卷积神经网络(VDCNN)架构,也称为VGG Net。VGG Net使用非常小的卷积滤波器,深度达到 16-19层。Conneau等人(2016)提出了另一种文本分类的VDCNN架构,使用小卷积和池化。他们声称这个VDCNN架构是第一个在文本处理中使用的,它在字符级别上起作用。该架构由29个卷积层组成。

Network In Network

Lin等人(2013)提出了网络中的网络(Network In Network,NIN)。NIN以具有复杂结构的微神经网络代替传统卷积神经网络(CNN)的卷积层。它使用多层感知器(MLPConv)处理微神经网络和全局平均池化层,而不是全连接层。深度NIN架构可以由NIN结构的多重叠加组成。

Region-based Convolutional Neural Networks

Girshick等人(2014)提出了基于区域的卷积神经网络(R-CNN),使用区域进行识别。R-CNN使用区域来定位和分割目标。该架构由三个模块组成:定义了候选区域的集合的类别独立区域建议,从区域中提取特征的大型卷积神经网络(CNN),以及一组类特定的线性支持向量机(SVM)。

Fast R-CNN

Girshick(2015)提出了快速的基于区域的卷积网络(Fast R-CNN)。这种方法利用R-CNN架构能快速地生成结果。Fast R-CNN由卷积层和池化层、区域建议层和一系列全连接层组成。

Faster R-CNN

Ren等人(2015)提出了更快的基于区域的卷积神经网络(Faster R-CNN),它使用区域建议网络(Region Proposal Network,RPN)进行实时目标检测。RPN是一个全卷积网络,能够准确、高效地生成区域建议(Ren et al.,2015)。

Mask R-CNN

何恺明等人(2017)提出了基于区域的掩模卷积网络(Mask R-CNN)实例目标分割。Mask R-CNN扩展了R-CNN的架构,并使用一个额外的分支用于预测目标掩模。

Multi-Expert R-CNN

Lee等人(2017)提出了基于区域的多专家卷积神经网络(ME R-CNN),利用了Fast R-CNN架构。ME R-CNN从选择性和详尽的搜索中生成兴趣区域(RoI)。它也使用per-RoI多专家网络而不是单一的per-RoI网络。每个专家都是来自Fast R-CNN的全连接层的相同架构。

Deep Residual Networks

He等人(2015)提出的残差网络(ResNet)由152层组成。ResNet具有较低的误差,并且容易通过残差学习进行训练。更深层次的ResNet可以获得更好的性能。在深度学习领域,人们认为ResNet是一个重要的进步。

Resnet in Resnet

Targ等人(2016)在Resnet in Resnet(RiR)中提出将ResNets和标准卷积神经网络(CNN)结合到深层双流架构中。

ResNeXt

Xie等人(2016)提出了ResNeXt架构。ResNext利用ResNets来重复使用分割-转换-合并策略。

Capsule Networks

Sabour等人(2017)提出了胶囊网络(CapsNet),即一个包含两个卷积层和一个全连接层的架构。CapsNet通常包含多个卷积层,胶囊层位于末端。CapsNet被认为是深度学习的最新突破之一,因为据说这是基于卷积神经网络的局限性而提出的。它使用的是一层又一层的胶囊,而不是神经元。激活的较低级胶囊做出预测,在同意多个预测后,更高级的胶囊变得活跃。在这些胶囊层中使用了一种协议路由机制。Hinton之后提出EM路由,利用期望最大化(EM)算法对CapsNet进行了改进。

Recurrent Neural Networks

循环神经网络(RNN)更适合于序列输入,如语音、文本和生成序列。一个重复的隐藏单元在时间展开时可以被认为是具有相同权重的非常深的前馈网络。由于梯度消失和维度爆炸问题,RNN曾经很难训练。为了解决这个问题,后来许多人提出了改进意见。
Goodfellow等人(2016)详细分析了循环和递归神经网络和架构的细节,以及相关的门控和记忆网络。
Karpathy等人(2015)使用字符级语言模型来分析和可视化预测、表征训练动态、RNN及其变体(如LSTM)的错误类型等。
J´ozefowicz等人(2016)探讨了RNN模型和语言模型的局限性。

RNN-EM

Peng和Yao(2015)提出了利用外部记忆(RNN-EM)来改善RNN的记忆能力。他们声称在语言理解方面达到了最先进的水平,比其他RNN更好。

GF-RNN

Chung等人(2015)提出了门控反馈递归神经网络(GF-RNN),它通过将多个递归层与全局门控单元叠加来扩展标准的RNN。

CRF-RNN

Zheng等人(2015)提出条件随机场作为循环神经网络(CRF-RNN),其将卷积神经网络(CNN)和条件随机场(CRF)结合起来进行概率图形建模。

Quasi-RNN

Bradbury等人(2016)提出了用于神经序列建模和沿时间步的并行应用的准循环神经网络(QRNN)。

Memory Networks

Weston等人(2014)提出了问答记忆网络(QA)。记忆网络由记忆、输入特征映射、泛化、输出特征映射和响应组成。

Dynamic Memory Networks

Kumar等人(2015)提出了用于QA任务的动态记忆网络(DMN)。DMN有四个模块:输入、问题、情景记忆、输出。

Augmented Neural Networks

Olah和Carter(2016)很好地展示了注意力和增强循环神经网络,即神经图灵机(NTM)、注意力接口、神经编码器和自适应计算时间。增强神经网络通常是使用额外的属性,如逻辑函数以及标准的神经网络架构。

Neural Turing Machines

Graves等人(2014)提出了神经图灵机(NTM)架构,由神经网络控制器和记忆库组成。NTM通常将RNN与外部记忆库结合。

Neural GPU

Kaiser和Sutskever(2015)提出了神经 GPU,解决了NTM的并行问题。

Neural Random-Access Machines

Kurach等人(2015)提出了神经随机存取机,它使用外部的可变大小的随机存取存储器。

Neural Programmer

Neelakantan等人(2015)提出了神经编程器,一种具有算术和逻辑功能的增强神经网络。

Neural Programmer-Interpreters

Reed和de Freitas(2015)提出了可以学习的神经编程器-解释器(NPI)。NPI包括周期性内核、程序内存和特定于领域的编码器。

Long Short Term Memory Networks

Hochreiter和Schmidhuber(1997)提出了长短期记忆(Long Short-Term Memory,LSTM),克服了循环神经网络(RNN)的误差回流问题。LSTM是基于循环网络和基于梯度的学习算法,LSTM引入自循环产生路径,使得梯度能够流动。
Greff等人(2017)对标准LSTM和8个LSTM变体进行了大规模分析,分别用于语音识别、手写识别和复调音乐建模。他们声称LSTM的8个变体没有显著改善,而只有标准LSTM表现良好。
Shi等人(2016)提出了深度长短期记忆网络(DLSTM),它是一个LSTM单元的堆栈,用于特征映射学习表示。

Batch-Normalized LSTM

Cooijmans等人(2016)提出了批归一化LSTM(BN-LSTM),它对递归神经网络的隐藏状态使用批归一化。

Pixel RNN

van den Oord等人(2016)提出像素递归神经网络(Pixel-RNN),由12个二维LSTM层组成。

Bidirectional LSTM

W¨ollmer等人(2010)提出了双向LSTM(BLSTM)的循环网络与动态贝叶斯网络(DBN)一起用于上下文敏感关键字检测。

Variational Bi-LSTM

Shabanian等人(2017)提出了变分双向LSTM(Variational Bi-LSTM),它是双向LSTM体系结构的变体。Variational Bi-LSTM使用变分自编码器(VAE)在LSTM之间创建一个信息交换通道,以学习更好的表征。

Googles Neural Machine Translation

Wu等人(2016)提出了名为谷歌神经机器翻译(GNMT)的自动翻译系统,该系统结合了编码器网络、解码器网络和注意力网络,遵循共同的序列对序列(sequence-to-sequence)的学习框架。

Fader Network

Lample等人(2017)提出了Fader网络,这是一种新型的编码器-解码器架构,通过改变属性值来生成真实的输入图像变化。

Hyper Networks

Ha等人(2016)提出的超网络(Hyper Networks)为其他神经网络生成权值,如静态超网络卷积网络、用于循环网络的动态超网络。
Deutsch(2018)使用超网络生成神经网络。

Highway Networks

Srivastava等人(2015)提出了高速路网络(Highway Networks),通过使用门控单元来学习管理信息。跨多个层次的信息流称为信息高速路。

Recurrent Highway Networks

Zilly等人(2017)提出了循环高速路网络(Recurrent Highway Networks,RHN),它扩展了长短期记忆(LSTM)架构。RHN在周期性过渡中使用了Highway层。

Highway LSTM RNN

Zhang等人(2016)提出了高速路长短期记忆Highway Long Short-Term Memory(HLSTM)RNN,它在相邻层的内存单元之间扩展了具有封闭方向连接(即Highway)的深度LSTM网络。

Long-Term Recurrent CNN

Donahue等人(2014)提出了长期循环卷积网络(LRCN),它使用CNN进行输入,然后使用LSTM进行递归序列建模并生成预测。

Deep Neural SVM

Zhang等人(2015)提出了深度神经SVM(DNSVM),它以支持向量机(Support Vector Machine,SVM)作为深度神经网络(Deep Neural Network,DNN)分类的顶层。

Convolutional Residual Memory Networks

Moniz和Pal(2016)提出了卷积残差记忆网络,将记忆机制并入卷积神经网络(CNN)。它用一个长短期记忆机制来增强卷积残差网络。

Fractal Networks

Larsson等人(2016)提出分形网络即FractalNet作为残差网络的替代方案。他们声称可以训练超深度的神经网络而不需要残差学习。分形是简单扩展规则生成的重复架构。

WaveNet

van den Oord等人(2016)提出了用于产生原始音频的深度神经网络WaveNet。WaveNet由一堆卷积层和softmax分布层组成,用于输出。
Rethage等人(2017)提出了一个WaveNet模型用于语音去噪。

Pointer Networks

Vinyals等人(2017)提出了指针网络(Ptr-Nets),通过使用一种称为“指针”的softmax概率分布来解决表征变量字典的问题。


Deep Generative Models

在本节中,我们将简要讨论其他深度架构,它们使用与深度神经网络类似的多个抽象层和表示层,也称为深度生成模型(deep generate Models,DGM)。Bengio(2009)解释了深层架构,例如Boltzmann machines(BM)和Restricted Boltzmann Machines(RBM)等及其变体。
Goodfellow等人(2016)详细解释了深度生成模型,如受限和非受限的玻尔兹曼机及其变种、深度玻尔兹曼机、深度信念网络(DBN)、定向生成网络和生成随机网络等。
Maaløe等人(2016)提出了辅助的深层生成模型(Auxiliary Deep Generative Models),在这些模型中,他们扩展了具有辅助变量的深层生成模型。辅助变量利用随机层和跳过连接生成变分分布。
Rezende等人(2016)开发了一种深度生成模型的单次泛化。

Boltzmann Machines

玻尔兹曼机是学习任意概率分布的连接主义方法,使用最大似然原则进行学习。

Restricted Boltzmann Machines

受限玻尔兹曼机(Restricted Boltzmann Machines,RBM)是马尔可夫随机场的一种特殊类型,包含一层随机隐藏单元,即潜变量和一层可观测变量。
Hinton和Salakhutdinov(2011)提出了一种利用受限玻尔兹曼机(RBM)进行文档处理的深度生成模型。

Deep Belief Networks

深度信念网络(Deep Belief Networks,DBN)是具有多个潜在二元或真实变量层的生成模型。
Ranzato等人(2011)利用深度信念网络(deep Belief Network,DBN)建立了深度生成模型进行图像识别。

Deep Lambertian Networks

Tang等人(2012)提出了深度朗伯网络(Deep Lambertian Networks,DLN),它是一个多层次的生成模型,其中潜在的变量是反照率、表面法线和光源。DLNis是朗伯反射率与高斯受限玻尔兹曼机和深度信念网络的结合。

Generative Adversarial Networks

Goodfellow等人(2014)提出了生成对抗网络(Generate Adversarial Nets,GAN),用于通过对抗过程来评估生成模型。GAN架构是由一个针对对手(即一个学习模型或数据分布的判别模型)的生成模型组成。Mao等人(2016)、Kim等人(2017)对GAN提出了更多的改进。
Salimans等人(2016)提出了几种训练GANs的方法。

Laplacian Generative Adversarial Networks

Denton等人(2015)提出了一种深度生成模型(DGM),叫做拉普拉斯生成对抗网络(LAPGAN),使用生成对抗网络(GAN)方法。该模型还在拉普拉斯金字塔框架中使用卷积网络。

Recurrent Support Vector Machines

Shi等人(2016)提出了循环支持向量机(RSVM),利用循环神经网络(RNN)从输入序列中提取特征,用标准支持向量机(SVM)进行序列级目标识别。


Training and Optimization Techniques

在本节中,我们将简要概述一些主要的技术,用于正则化和优化深度神经网络(DNN)。

Dropout

Srivastava等人(2014)提出Dropout,以防止神经网络过拟合。Dropout是一种神经网络模型平均正则化方法,通过增加噪声到其隐藏单元。在训练过程中,它会从神经网络中随机抽取出单元和连接。Dropout可以用于像RBM(Srivastava et al.,2014)这样的图形模型中,也可以用于任何类型的神经网络。最近提出的一个关于Dropout的改进是Fraternal Dropout,用于循环神经网络(RNN)。

Maxout

Goodfellow等人(2013)提出Maxout,一种新的激活函数,用于Dropout。Maxout的输出是一组输入的最大值,有利于Dropout的模型平均。

Zoneout

Krueger等人(2016)提出了循环神经网络(RNN)的正则化方法Zoneout。Zoneout在训练中随机使用噪音,类似于Dropout,但保留了隐藏的单元而不是丢弃。

Deep Residual Learning

He等人(2015)提出了深度残差学习框架,该框架被称为低训练误差的ResNet。

Batch Normalization

Ioffe和Szegedy(2015)提出了批归一化,通过减少内部协变量移位来加速深度神经网络训练的方法。Ioffe(2017)提出批重归一化,扩展了以前的方法。

Distillation

Hinton等人(2015)提出了将知识从高度正则化模型的集合(即神经网络)转化为压缩小模型的方法。

Layer Normalization

Ba等人(2016)提出了层归一化,特别是针对RNN的深度神经网络加速训练,解决了批归一化的局限性。


Deep Learning frameworks

有大量的开源库和框架可供深度学习使用。它们大多数是为Python编程语言构建的。如Theano、Tensorflow、PyTorch、PyBrain、Caffe、Blocks and Fuel、CuDNN、Honk、ChainerCV、PyLearn2、Chainer、torch等。


Applications of Deep Learning

在本节中,我们将简要地讨论一些最近在深度学习方面的杰出应用。自深度学习(DL)开始以来,DL方法以监督、非监督、半监督或强化学习的形式被广泛应用于各个领域。从分类和检测任务开始,DL应用正在迅速扩展到每一个领域。
例如:

  • 图像分类与识别
  • 视频分类
  • 序列生成
  • 缺陷分类
  • 文本、语音、图像和视频处理
  • 文本分类
  • 语音处理
  • 语音识别和口语理解
  • 文本到语音生成
  • 查询分类
  • 句子分类
  • 句子建模
  • 词汇处理
  • 预选择
  • 文档和句子处理
  • 生成图像文字说明
  • 照片风格迁移
  • 自然图像流形
  • 图像着色
  • 图像问答
  • 生成纹理和风格化图像
  • 视觉和文本问答
  • 视觉识别和描述
  • 目标识别
  • 文档处理
  • 人物动作合成和编辑
  • 歌曲合成
  • 身份识别
  • 人脸识别和验证
  • 视频动作识别
  • 人类动作识别
  • 动作识别
  • 分类和可视化动作捕捉序列
  • 手写生成和预测
  • 自动化和机器翻译
  • 命名实体识别
  • 移动视觉
  • 对话智能体
  • 调用遗传变异
  • 癌症检测
  • X射线CT重建
  • 癫痫发作预测
  • 硬件加速
  • 机器人

等。
Deng和Yu(2014)提供了DL在语音处理、信息检索、目标识别、计算机视觉、多模态、多任务学习等领域应用的详细列表。
使用深度强化学习(Deep Reinforcement Learning,DRL)来掌握游戏已经成为当今的一个热门话题。每到现在,人工智能机器人都是用DNN和DRL创建的,它们在战略和其他游戏中击败了人类世界冠军和象棋大师,从几个小时的训练开始。例如围棋的AlphaGo和AlphaGo Zero。


Discussion

尽管深度学习在许多领域取得了巨大的成功,但它还有很长的路要走。还有很多地方有待改进。至于局限性,例子也是相当多的。例如:Nguyen等人表明深度神经网络(DNN)在识别图像时容易被欺骗。还有其他问题,如Yosinski等人提出的学习的特征可迁移性。Huang等人提出了一种神经网络攻击防御的体系结构,认为未来的工作需要防御这些攻击。Zhang等人则提出了一个理解深度学习模型的实验框架,他们认为理解深度学习需要重新思考和概括。
Marcus在2018年对深度学习(Deep Learning,DL)的作用、局限性和本质进行了重要的回顾。他强烈指出了DL方法的局限性,即需要更多的数据,容量有限,不能处理层次结构,无法进行开放式推理,不能充分透明,不能与先验知识集成,不能区分因果关系。他还提到,DL假设了一个稳定的世界,以近似方法实现,工程化很困难,并且存在着过度炒作的潜在风险。Marcus认为DL需要重新概念化,并在非监督学习、符号操作和混合模型中寻找可能性,从认知科学和心理学中获得见解,并迎接更大胆的挑战。


Conclusion

尽管深度学习(DL)比以往任何时候都更快地推进了世界的发展,但仍有许多方面值得我们去研究。我们仍然无法完全地理解深度学习,我们如何让机器变得更聪明,更接近或比人类更聪明,或者像人类一样学习。DL一直在解决许多问题,同时将技术应用到方方面面。但是人类仍然面临着许多难题,例如仍有人死于饥饿和粮食危机,癌症和其他致命的疾病等。我们希望深度学习和人工智能将更加致力于改善人类的生活质量,通过开展最困难的科学研究。最后但也是最重要的,愿我们的世界变得更加美好。


碰到底线咯 后面没有啦

本文标题:deep learning笔记:近年来深度学习的重要研究成果

文章作者:高深远

发布时间:2020年02月15日 - 07:19

最后更新:2020年02月16日 - 07:14

原始链接:https://gsy00517.github.io/deep-learning20200215071915/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
front end笔记:制作web时的一些小技巧与小问题 | 高深远的博客

front end笔记:制作web时的一些小技巧与小问题

大一有一段时间,我沉迷于web前端制作网页,比较熟练地掌握了html的语法,还根据需要接触了一些CSS以及js的内容。说白了,html只是一种标记语言(不属于编程语言),但是它简单易学,且很容易获得可视化的效果,对于培养兴趣而言我感觉是很有帮助的。油管up主,现哈佛在读学霸John Fish(请科学上网)当初就是从html进入计算机世界的。下面贴一个我自己做的网页,是综合web三大语言编写的,大一的时候把自己需要的网站都放上面了,也有一种归属感吧。



主页上那个是python之禅,也是我很喜欢的一段文字,在python环境下import this就可以看到。由于上面的网页是我学web的时候边看书边编的,各种元素都尝试了一下,最后也没有美化一直到现在,所以大佬们勿喷哈。


小技巧

我这里强烈推荐使用VScode写前端,它有很多强大的插件,我这里推荐其中一个吧。

如介绍所写,使用alt+B快捷键可以直接在默认浏览器下查看你写的网页,而shift+alt+B可以选浏览器查看,因为有些时候microsoft自带的edge浏览器无法实现你编写的效果(巨坑),推荐使用chrome打开浏览。使用这个插件能让你更快捷地预览你编写的效果并进行修改,大大提高了效率。
其他的插件网上有很多推荐,也等待着你自己去发现,这里就不一一列出了。
还有一个快捷的操作就是快速生成代码块,在VScode中是这样操作的(其他编辑器也应该类似):

  1. 输入一个
  2. 按tab键或者回车: 这样就可以节省很多时间,非常方便。

小问题

在我想使用web来打开我本地的txt文件时,我遇到过这样一个问题:打开的中文文档在浏览器中显示为乱码。在尝试其他浏览器后,我发现这不是浏览器的问题。最后我大致找到了两种解决办法:

  1. 一种是找到head下面的meta charset,修改代码如下:
  2. 另一种是另存为文件时修改一下格式,这里我们修改成“UTF-8”。另外我室友在使用python导入文件的时候也因为格式导致报错,修改成“ANSI”后即可。 希望通过上面两种方法的尝试能让你解决乱码问题,几种编码格式的区别在这里暂不说明,以后有空补上。

碰到底线咯 后面没有啦

本文标题:front end笔记:制作web时的一些小技巧与小问题

文章作者:高深远

发布时间:2019年09月14日 - 08:02

最后更新:2020年02月07日 - 16:55

原始链接:https://gsy00517.github.io/front-end20190914080224/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
front end笔记:制作web时的一些小技巧与小问题 | 高深远的博客

front end笔记:制作web时的一些小技巧与小问题

大一有一段时间,我沉迷于web前端制作网页,比较熟练地掌握了html的语法,还根据需要接触了一些CSS以及js的内容。说白了,html只是一种标记语言(不属于编程语言),但是它简单易学,且很容易获得可视化的效果,对于培养兴趣而言我感觉是很有帮助的。油管up主,现哈佛在读学霸John Fish(请科学上网)当初就是从html进入计算机世界的。下面贴一个我自己做的网页,是综合web三大语言编写的,大一的时候把自己需要的网站都放上面了,也有一种归属感吧。



主页上那个是python之禅,也是我很喜欢的一段文字,在python环境下import this就可以看到。由于上面的网页是我学web的时候边看书边编的,各种元素都尝试了一下,最后也没有美化一直到现在,所以大佬们勿喷哈。


小技巧

我这里强烈推荐使用VScode写前端,它有很多强大的插件,我这里推荐其中一个吧。

如介绍所写,使用alt+B快捷键可以直接在默认浏览器下查看你写的网页,而shift+alt+B可以选浏览器查看,因为有些时候microsoft自带的edge浏览器无法实现你编写的效果(巨坑),推荐使用chrome打开浏览。使用这个插件能让你更快捷地预览你编写的效果并进行修改,大大提高了效率。
其他的插件网上有很多推荐,也等待着你自己去发现,这里就不一一列出了。
还有一个快捷的操作就是快速生成代码块,在VScode中是这样操作的(其他编辑器也应该类似):

  1. 输入一个
  2. 按tab键或者回车: 这样就可以节省很多时间,非常方便。

小问题

在我想使用web来打开我本地的txt文件时,我遇到过这样一个问题:打开的中文文档在浏览器中显示为乱码。在尝试其他浏览器后,我发现这不是浏览器的问题。最后我大致找到了两种解决办法:

  1. 一种是找到head下面的meta charset,修改代码如下:
  2. 另一种是另存为文件时修改一下格式,这里我们修改成“UTF-8”。另外我室友在使用python导入文件的时候也因为格式导致报错,修改成“ANSI”后即可。 希望通过上面两种方法的尝试能让你解决乱码问题,几种编码格式的区别在这里暂不说明,以后有空补上。

碰到底线咯 后面没有啦

本文标题:front end笔记:制作web时的一些小技巧与小问题

文章作者:高深远

发布时间:2019年09月14日 - 08:02

最后更新:2020年02月07日 - 16:55

原始链接:https://gsy00517.github.io/front-end20190914080224/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
front end笔记:使用div实现居中显示 | 高深远的博客

front end笔记:使用div实现居中显示

最近心血来潮花了好久给自己的博客添加了一个粒子时钟,最后想要使它在sidebar中居中显示废了我好大功夫,为了以后不在这上面浪费时间,我决定浪费现在的时间把这个问题记下来。

References

电子文献:
https://www.jianshu.com/p/f0bffc42c1ce
https://blog.csdn.net/chwshuang/article/details/52350559


swig

由于我要将我的时钟显示在侧边栏,需要插入到header.swig文件中。hexo博客的源码中有大量这个格式的文件,然而具体的使用方法我也不是很清楚。在查阅了一些文章之后,我对swig有了一个初步的认知。
swig是一个JS前端模板引擎,它有如下特点:

  • 支持大多数主流浏览器。
  • 表达式兼容性好。
  • 面向对象的模板继承。
  • 将过滤器和转换应用到模板中的输出。
  • 可根据路劲渲染页面。
  • 支持页面复用。
  • 支持动态页面。
  • 可扩展、可定制。

可以通过VSchart将swig与其它前端模板框架进行对比,这个网站由维基支持,可以在里面进行各种对比,非常有意思,在这里推荐一下。
关于swig的基本用法,可以在我文首的第一个参考链接中找到,个人认为不搞前端的话大概率是用不到的。


原本的方法

当我添加完时钟本地测验时,我发现添加的时钟在侧边栏的位置没有居中。

根据之前学习html的记忆,我尝试了使用<p align=center></p>标签包裹我的插入语句,然而并没有达到想要的效果。


使用div实现居中

为了达到上述的目的,我使用<div>标签来分割出块,并使用div的属性来实现居中显示的效果。

1
2
3
<div style="Text-align:center;width:100%;">
{% include '../_custom/clock.swig' %}
</div>


使用nav实现隐藏

本以为大功告成,结果在移动端查看时,发现竖屏显示效果非常煞风景。

其实在电脑端浏览器也可以预览移动端的效果,方法很简单,就是直接将浏览器窗口小化,减小两边间距,网页就会自动变成竖屏显示的状态(除非没有)。此外,当我们F12检查元素或者查看源时,网页也会被挤到一侧从而变成竖屏显示的状态。

注:这里补充一下检查元素和查看源之间的区别,一般的浏览器右键都会有这两个功能,表面上看起来似乎也差不多,但是它们还是有区别的。
检查元素看的是渲染过的最终代码,可以做到定位网页元素、实时监控网页元素属性变化的功能,可以及时调试、修改、定位、追踪检查、查看嵌套,修改样式和查看js动态输出信息。这让我想起了自己当初就是这样直接修改四级成绩,然后骗朋友的,不知道的人还真的想不出这原因,就以为的确是真的啦哈哈。
另一方面,查看源只是把网页输出的源代码,即就是别人服务器发送到浏览器的原封不动的代码直接打开,既不能动态变化,也不能修改。

为了解决这个问题,追求美观,我就想到可以把时钟和标签、分类等菜单中的索引一起,在竖屏状态下不点击时就不显示。在分析了header.swig中菜单部分的源码之后,我注意到一个标签<nav>,它是是HTML5的新标签,可以标注一个导航链接的区域。于是,我将插入时钟的语句移入nav所包裹的块中,就完美达到了我的需求。


意外的问题

在我写这篇博文的时候,出现了一个奇怪的问题。每当我想要本地预览(部署应该也会出现这个问题),都会报错:“Nunjucks Error: [Line 17, Column 239] unexpected token: }”,这就让我非常的苦恼。
根据错误信息,我开始一个一个寻找我文中的花括号。在反复删减和搜索相关问题之后,我发现是我插入在行间的一个include的swig语句惹的祸(我要是写出来又报错,插入在段间就没问题,可以到上文找)。
这类异常一般是文章中使用了大括号{}这个特殊字符,且没有转义导致编译不通过,解决的办法是使用&#123; &#125;对大的花括号进行转换。

补充:小的圆括号可用&#40; &#41;进行转换。

没有这类问题当然再好不过啦,如果出现了,可以试试上面的方法。这类涉及转义的符号还是得熟悉其规则,避免老是出错。


碰到底线咯 后面没有啦

本文标题:front end笔记:使用div实现居中显示

文章作者:高深远

发布时间:2019年11月10日 - 16:59

最后更新:2020年02月18日 - 12:43

原始链接:https://gsy00517.github.io/front-end20191110165901/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
front end笔记:使用div实现居中显示 | 高深远的博客

front end笔记:使用div实现居中显示

最近心血来潮花了好久给自己的博客添加了一个粒子时钟,最后想要使它在sidebar中居中显示废了我好大功夫,为了以后不在这上面浪费时间,我决定浪费现在的时间把这个问题记下来。

References

电子文献:
https://www.jianshu.com/p/f0bffc42c1ce
https://blog.csdn.net/chwshuang/article/details/52350559


swig

由于我要将我的时钟显示在侧边栏,需要插入到header.swig文件中。hexo博客的源码中有大量这个格式的文件,然而具体的使用方法我也不是很清楚。在查阅了一些文章之后,我对swig有了一个初步的认知。
swig是一个JS前端模板引擎,它有如下特点:

  • 支持大多数主流浏览器。
  • 表达式兼容性好。
  • 面向对象的模板继承。
  • 将过滤器和转换应用到模板中的输出。
  • 可根据路劲渲染页面。
  • 支持页面复用。
  • 支持动态页面。
  • 可扩展、可定制。

可以通过VSchart将swig与其它前端模板框架进行对比,这个网站由维基支持,可以在里面进行各种对比,非常有意思,在这里推荐一下。
关于swig的基本用法,可以在我文首的第一个参考链接中找到,个人认为不搞前端的话大概率是用不到的。


原本的方法

当我添加完时钟本地测验时,我发现添加的时钟在侧边栏的位置没有居中。

根据之前学习html的记忆,我尝试了使用<p align=center></p>标签包裹我的插入语句,然而并没有达到想要的效果。


使用div实现居中

为了达到上述的目的,我使用<div>标签来分割出块,并使用div的属性来实现居中显示的效果。

1
2
3
<div style="Text-align:center;width:100%;">
{% include '../_custom/clock.swig' %}
</div>


使用nav实现隐藏

本以为大功告成,结果在移动端查看时,发现竖屏显示效果非常煞风景。

其实在电脑端浏览器也可以预览移动端的效果,方法很简单,就是直接将浏览器窗口小化,减小两边间距,网页就会自动变成竖屏显示的状态(除非没有)。此外,当我们F12检查元素或者查看源时,网页也会被挤到一侧从而变成竖屏显示的状态。

注:这里补充一下检查元素和查看源之间的区别,一般的浏览器右键都会有这两个功能,表面上看起来似乎也差不多,但是它们还是有区别的。
检查元素看的是渲染过的最终代码,可以做到定位网页元素、实时监控网页元素属性变化的功能,可以及时调试、修改、定位、追踪检查、查看嵌套,修改样式和查看js动态输出信息。这让我想起了自己当初就是这样直接修改四级成绩,然后骗朋友的,不知道的人还真的想不出这原因,就以为的确是真的啦哈哈。
另一方面,查看源只是把网页输出的源代码,即就是别人服务器发送到浏览器的原封不动的代码直接打开,既不能动态变化,也不能修改。

为了解决这个问题,追求美观,我就想到可以把时钟和标签、分类等菜单中的索引一起,在竖屏状态下不点击时就不显示。在分析了header.swig中菜单部分的源码之后,我注意到一个标签<nav>,它是是HTML5的新标签,可以标注一个导航链接的区域。于是,我将插入时钟的语句移入nav所包裹的块中,就完美达到了我的需求。


意外的问题

在我写这篇博文的时候,出现了一个奇怪的问题。每当我想要本地预览(部署应该也会出现这个问题),都会报错:“Nunjucks Error: [Line 17, Column 239] unexpected token: }”,这就让我非常的苦恼。
根据错误信息,我开始一个一个寻找我文中的花括号。在反复删减和搜索相关问题之后,我发现是我插入在行间的一个include的swig语句惹的祸(我要是写出来又报错,插入在段间就没问题,可以到上文找)。
这类异常一般是文章中使用了大括号{}这个特殊字符,且没有转义导致编译不通过,解决的办法是使用&#123; &#125;对大的花括号进行转换。

补充:小的圆括号可用&#40; &#41;进行转换。

没有这类问题当然再好不过啦,如果出现了,可以试试上面的方法。这类涉及转义的符号还是得熟悉其规则,避免老是出错。


碰到底线咯 后面没有啦

本文标题:front end笔记:使用div实现居中显示

文章作者:高深远

发布时间:2019年11月10日 - 16:59

最后更新:2020年02月18日 - 12:43

原始链接:https://gsy00517.github.io/front-end20191110165901/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
game笔记:海贼王燃烧之血键盘操作 | 高深远的博客

game笔记:海贼王燃烧之血键盘操作

这是一篇关于海贼王也关于游戏的文章,出于对海贼王狂热的喜爱,我决定还是在博客里添一篇这样的文章,感兴趣可以看看。


画展

国庆期间,我留在了武汉。幸运的是,海贼王官方在大陆的首次巡展“路飞来了”正好此时也在武汉开展。作为一名海贼铁粉,我当然是毫不犹豫地买了票。其实根据我博客网站的icon以及我目前的个人头像,应该很容易看出我对海贼王的热爱哈哈。

去的时候快接近饭点了,人还是不少,都是真爱啊~不过像我这样我单身一人的占比不大,但也有。让我惊讶的是当我看着日文的原稿时竟能直接反应出中文,果然那么多年来全套漫画没白买。期间跟一位貌似是艺术生的小哥聊得挺开的,只可惜最后没留联系方式,有缘再见吧。
整个展看下来还是挺震撼的,尤其是刚进去的时候,激动地鸡皮疙瘩都起来了。不过跟我在东京塔下面的海贼王主题乐园激动得哭出来还是有一定差距的。看完展之后我买了几张原稿的复刻版,花了不少钱,但觉得挺值,珍藏了。我是一个漫画党,除了剧场版或特别篇之外我看的都是漫画(不过是通过动画入坑的,星空卫视司法岛,一代人的记忆哈哈)。说实话,尾田构思之精巧,漫画史上无人能及,感兴趣可以看看知乎上关于尾田构思的讨论,漫画真的埋了很多神一般的线索,这是动画里办不到的,细细看很有意思。


燃烧之血

看完展,我心中对于海贼王的热血再一次得到激发,回学校就打开燃烧之血,回到海贼世界过把瘾。
海贼王燃烧之血(One Piece:Burning Blood)是16年发行的一款海贼王题材的格斗游戏,个人觉得其中的自然系元素化以及霸气设定真的太棒了!另外各种招式都还原得很全很细致,简直就是一边玩一边享受精彩的画面。文末提供了一些图,可以欣赏一下,真的很赞。
由于steam版价格原因(加上全部DLC需两三百rmb)以及原本这游戏好像是在游戏机上的(PC版是移植的),导致PC键盘操作方式的教程不是很全,因此本篇文章主要就是对该款游戏的按键操作做一个补充。


按键操作

十几个小时玩下来,基本的按键摸得比较熟了,其实键盘操作也有键盘的优势,熟练就好。
首先是很普适的移动方式:
前进 W
后退 S
左行 A
右行 D
下面是一些基本的战斗操作:
攻击 K
重击 O
跳跃 L
防御 ;(分号键)
往后换人 E
往前换人 I
突破极限状态 右ctrl
必杀技(突破极限状态下) 右ctrl
如果要使用招式,那么按下Q,在战斗界面的左侧就会出现招式列表,即三个招式的名称及按键操作,按住Q不松,再配合对应按键,就可以使出对应的招式。
招式一 (招式列表情况下) K
招式二 (招式列表情况下) O
招式三 (招式列表情况下) ;
一般情况下,长按对应键不松可以延迟招式的释放时间(比如在对手倒地时可以尝试)。此外,一些招式延迟附带蓄力效果,可以打出更强的攻击(附带破防效果)。
接下来是一些组合按键的操作:
破防 K+L
重击破防 O+;
侧步闪躲 W、S、A、D+;
范围攻击 S+K
范围重击 S+O
跳跃攻击 L+K
跳跃重击 L+O
有些角色还拥有特殊的衍生技能,需通过一定的按键组合释放,这里举两个例子,别的可以参考收藏图鉴:
艾斯 神火•不知火 L+O
白胡子 垂直跳跃攻击 L+O
接下来就是非常有特色的能力啦,按住P键就可以开启自然系能力者的元素化,可以轻松躲掉普攻并适时反击。如果有霸气的话,按住P也可开启霸气,在期间进行攻击就可以造成更大伤害,也不用惧怕自然系了。此外,一些角色开启能力时还可以实现特定的能力,作为海贼迷真的感动到哭,下面举几个例子:
女帝 快速后闪 P+L
黄猿 瞬移 P+L+方向
大熊 瞬移 P+L
白胡子 双震 P+招式一

白胡子的双震是我最喜欢的技能,真的非常有打击感和冲击力,上图截自b站up主的操作教程,我的许多操作都是从那学来的,他在b站和爱奇艺上都有很详细的连招教程,同时配音也很逗,感兴趣的话可以去观摩一下。


角色特点

这里补充一些目前我发现的角色的特点,也是只有海贼迷才懂的,可以说这游戏做得真的赞,后续我发现更多的话会继续补充。
1.众所周知,路飞无法被女帝石化。
2.山治对抗女性角色时,只能对女性示爱,因此只有挨打的份。


画面欣赏

静态无声的画面比起动态有声的还是差多了,但依旧不影响其魅力,看着就觉得很兴奋啦~













当然,游戏仅是起娱乐作用,劳逸结合是关键。如果你也热爱海贼王的话,欢迎和我交流!


碰到底线咯 后面没有啦

本文标题:game笔记:海贼王燃烧之血键盘操作

文章作者:高深远

发布时间:2019年10月07日 - 17:29

最后更新:2019年12月01日 - 22:27

原始链接:https://gsy00517.github.io/game20191007172952/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
game笔记:海贼王燃烧之血键盘操作 | 高深远的博客

game笔记:海贼王燃烧之血键盘操作

这是一篇关于海贼王也关于游戏的文章,出于对海贼王狂热的喜爱,我决定还是在博客里添一篇这样的文章,感兴趣可以看看。


画展

国庆期间,我留在了武汉。幸运的是,海贼王官方在大陆的首次巡展“路飞来了”正好此时也在武汉开展。作为一名海贼铁粉,我当然是毫不犹豫地买了票。其实根据我博客网站的icon以及我目前的个人头像,应该很容易看出我对海贼王的热爱哈哈。

去的时候快接近饭点了,人还是不少,都是真爱啊~不过像我这样我单身一人的占比不大,但也有。让我惊讶的是当我看着日文的原稿时竟能直接反应出中文,果然那么多年来全套漫画没白买。期间跟一位貌似是艺术生的小哥聊得挺开的,只可惜最后没留联系方式,有缘再见吧。
整个展看下来还是挺震撼的,尤其是刚进去的时候,激动地鸡皮疙瘩都起来了。不过跟我在东京塔下面的海贼王主题乐园激动得哭出来还是有一定差距的。看完展之后我买了几张原稿的复刻版,花了不少钱,但觉得挺值,珍藏了。我是一个漫画党,除了剧场版或特别篇之外我看的都是漫画(不过是通过动画入坑的,星空卫视司法岛,一代人的记忆哈哈)。说实话,尾田构思之精巧,漫画史上无人能及,感兴趣可以看看知乎上关于尾田构思的讨论,漫画真的埋了很多神一般的线索,这是动画里办不到的,细细看很有意思。


燃烧之血

看完展,我心中对于海贼王的热血再一次得到激发,回学校就打开燃烧之血,回到海贼世界过把瘾。
海贼王燃烧之血(One Piece:Burning Blood)是16年发行的一款海贼王题材的格斗游戏,个人觉得其中的自然系元素化以及霸气设定真的太棒了!另外各种招式都还原得很全很细致,简直就是一边玩一边享受精彩的画面。文末提供了一些图,可以欣赏一下,真的很赞。
由于steam版价格原因(加上全部DLC需两三百rmb)以及原本这游戏好像是在游戏机上的(PC版是移植的),导致PC键盘操作方式的教程不是很全,因此本篇文章主要就是对该款游戏的按键操作做一个补充。


按键操作

十几个小时玩下来,基本的按键摸得比较熟了,其实键盘操作也有键盘的优势,熟练就好。
首先是很普适的移动方式:
前进 W
后退 S
左行 A
右行 D
下面是一些基本的战斗操作:
攻击 K
重击 O
跳跃 L
防御 ;(分号键)
往后换人 E
往前换人 I
突破极限状态 右ctrl
必杀技(突破极限状态下) 右ctrl
如果要使用招式,那么按下Q,在战斗界面的左侧就会出现招式列表,即三个招式的名称及按键操作,按住Q不松,再配合对应按键,就可以使出对应的招式。
招式一 (招式列表情况下) K
招式二 (招式列表情况下) O
招式三 (招式列表情况下) ;
一般情况下,长按对应键不松可以延迟招式的释放时间(比如在对手倒地时可以尝试)。此外,一些招式延迟附带蓄力效果,可以打出更强的攻击(附带破防效果)。
接下来是一些组合按键的操作:
破防 K+L
重击破防 O+;
侧步闪躲 W、S、A、D+;
范围攻击 S+K
范围重击 S+O
跳跃攻击 L+K
跳跃重击 L+O
有些角色还拥有特殊的衍生技能,需通过一定的按键组合释放,这里举两个例子,别的可以参考收藏图鉴:
艾斯 神火•不知火 L+O
白胡子 垂直跳跃攻击 L+O
接下来就是非常有特色的能力啦,按住P键就可以开启自然系能力者的元素化,可以轻松躲掉普攻并适时反击。如果有霸气的话,按住P也可开启霸气,在期间进行攻击就可以造成更大伤害,也不用惧怕自然系了。此外,一些角色开启能力时还可以实现特定的能力,作为海贼迷真的感动到哭,下面举几个例子:
女帝 快速后闪 P+L
黄猿 瞬移 P+L+方向
大熊 瞬移 P+L
白胡子 双震 P+招式一

白胡子的双震是我最喜欢的技能,真的非常有打击感和冲击力,上图截自b站up主的操作教程,我的许多操作都是从那学来的,他在b站和爱奇艺上都有很详细的连招教程,同时配音也很逗,感兴趣的话可以去观摩一下。


角色特点

这里补充一些目前我发现的角色的特点,也是只有海贼迷才懂的,可以说这游戏做得真的赞,后续我发现更多的话会继续补充。
1.众所周知,路飞无法被女帝石化。
2.山治对抗女性角色时,只能对女性示爱,因此只有挨打的份。


画面欣赏

静态无声的画面比起动态有声的还是差多了,但依旧不影响其魅力,看着就觉得很兴奋啦~













当然,游戏仅是起娱乐作用,劳逸结合是关键。如果你也热爱海贼王的话,欢迎和我交流!


碰到底线咯 后面没有啦

本文标题:game笔记:海贼王燃烧之血键盘操作

文章作者:高深远

发布时间:2019年10月07日 - 17:29

最后更新:2019年12月01日 - 22:27

原始链接:https://gsy00517.github.io/game20191007172952/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:开始创建个人博客——方法及原因 | 高深远的博客

hexo笔记:开始创建个人博客——方法及原因

大家好,这是我的第一篇博文,这也是我的第一个自己搭建的网站,既然搭了,那第一篇就讲讲我搭建的过程吧。


安装步骤

  1. 安装node.js

    进入官网
    选择对应系统(我这里用win10),选择LTS(长期支持版本)安装,安装步骤中一直选择next即可。
    安装完后就可以把安装包删除了。
  2. 安装git

    进入官网
    选择对应系统的版本下载,同样也是按默认安装。
    安装成功后,你会在开始菜单中看到git文件夹。 其中Git Bash与linux和mac中的终端类似,它是git自带的程序,提供了linux风格的shell,我们可以在这里面执行相应的命令。

    注意:bash中的复制粘贴操作与linux中类似,ctrl+C用于终止进程,可以用鼠标中键进行粘贴操作。不嫌麻烦的话可以使用ctrl+shift+Cctrl+shift+V进行复制粘贴操作。

  3. 安装hexo

    hexo是一个快速、简洁且高效的博客框架,在这里我们使用hexo来搭建博客。
    首先,新建一个名为“blog”的空文件夹,以后我们的操作都在这个文件夹里进行,可以在bash中使用pwd命令查看当前所处位置。
    创建这个文件夹的目的是万一因为创建的博客出现问题或者不满意想重来等原因可以直接简单地把文件夹删除,也方便了对整个网站本地内容的移动。
    打开新建的文件夹,右键空白处,选择Git Bash Here。 接下来我们输入两行命令来验证node.js是否安装成功。 如出现如图所示结果,则表明安装成功。
    为了提高以后的下载速度,我们需要安装cnpm。cnpm是淘宝的国内镜像,因为npm的服务器位于国外有时可能会影响安装。
    继续在bash中输入npm install -g cnpm --registry=https://registry.npm.taobao.org安装cnpm。
    检验安装是否成功,输入cnpm
    如果输出结果显示如下,说明安装成功。 接下来我们安装hexo,输入命令cnpm install -g hexo-cli
    和上面一样,我们可以用hexo -v来验证是否成功安装hexo,这里就不贴图了。
    接下来我们输入hexo init来初始化建立整个项目。
    你会发现你的文件夹中多了许多文件,你也可以用ls -l命令来看到新增的文件。
  4. 完成本地环境的搭建

    至此,我们已经完成了本地环境的搭建,在这里,我想先介绍hexo中常用的命令。
    hexo n "文章标题":用于创建新的博文(欲删除文章,直接删除md文件并用下面的命令更新即可)。
    hexo s也就是hexo start:hexo会监视文件变动并自动更新,通过所给的地址localhost:4000/就可以直接在本地预览更新后的网站了。这里不需要generate,保存修改后的本地文件后可以直接看到效果。
    部署到远端服务器三步曲:

    1
    2
    3
    hexo clean #清除缓存,网页正常情况下可以忽略此条命令,执行该指令后,会删掉站点根目录下的public文件夹。
    hexo g #generate静态网页(静态网页这里指没有前端后端的网页而不是静止),该命令把md编译为html并存到public文件目录下。
    hexo d #将本地的更改部署到远端服务器(需要一点时间,请过一会再刷新网页)。

    此外,上面最后两步也可以使用hexo g -d直接代替。
    如果出现ERROR Deployer not found: git报错,可以使用npm install --save hexo-deployer-git命令解决。

    注意:由于部署到远端输入密码时密码不可见,有时候会导致部署失败,只有出现INFO Deploy done: git的结果才表明部署成功,否则再次部署重输密码即可。

    现在我们在bash中运行hexo s,打开浏览器,输入localhost:4000/,就可以看到hexo默认创建的页面了。

  5. 部署到远端服务器

    为了让别人能访问到你搭建的网站,我们需要部署到远端服务器。
    这里有两种选择,一种是部署到github上,新建一个repository,然后创建一个xxxxx.github.io域名(这里xxxxx必须为你的github用户名)。
    另一种选择是部署到国内的coding,这是考虑到访问速度的问题,不过我选择的是前者,亲测并没感觉有速度的困扰。
    个人比较推荐用github pages创建个人博客。
    部署这块网上有许多教程,这里不详细解释了,以后有机会补上。
    在部署的时候涉及到对主题配置文件的操作,linux和mac用户可以使用vim进行编辑,不过也可以使用VScode、sublime等代码编辑器进行操作。

    注:为了国内的访问速度,我最后添加了coding/github双线部署,两者的操作方式大同小异。值得注意的是,如果使用的是leancloud的第三方阅读量与评论统计系统,那么还得在leancloud的安全中心中添加coding的web域名。此外,部署到远端之后(特别是第一次)可能要等待几分钟才能访问到更新之后的页面。


创建原因

首先说明,我只是一个刚起步的很萌的萌新,懂得不多,别喷我哈~
步入大二,虽然我是大学才算真正接触编程,但一年多下来我也接触并且学习了不少技术知识。接触的多了、遇到的问题也复杂了起来,导致每次百度到的答案不一定能够解决我遇到的问题。此外,之前在学习编程语言、操作系统、ML、DL等知识的时候,为方便起见利用文本记了些笔记。然而笔记分散在四处,不方便管理与查看,因此就萌生了写博客的想法。由于个人比较喜欢自由DIY,所以没有使用CSDN、博客园等知名技术博客网站。
最后还是非常感谢我们华科的校友程序羊在b站和其他站点上分享的各种经验,我就是通过他的视频来搭建起自己的第一个博客网站的。他的其他视频也给了我很多启迪。
最后,最关键的原因,还是因为今天中秋节有空闲的时间哈哈,祝大家节日快乐!


碰到底线咯 后面没有啦

本文标题:hexo笔记:开始创建个人博客——方法及原因

文章作者:高深远

发布时间:2019年09月13日 - 15:33

最后更新:2020年02月27日 - 07:48

原始链接:https://gsy00517.github.io/hexo20190913153310/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:开始创建个人博客——方法及原因 | 高深远的博客

hexo笔记:开始创建个人博客——方法及原因

大家好,这是我的第一篇博文,这也是我的第一个自己搭建的网站,既然搭了,那第一篇就讲讲我搭建的过程吧。


安装步骤

  1. 安装node.js

    进入官网
    选择对应系统(我这里用win10),选择LTS(长期支持版本)安装,安装步骤中一直选择next即可。
    安装完后就可以把安装包删除了。
  2. 安装git

    进入官网
    选择对应系统的版本下载,同样也是按默认安装。
    安装成功后,你会在开始菜单中看到git文件夹。 其中Git Bash与linux和mac中的终端类似,它是git自带的程序,提供了linux风格的shell,我们可以在这里面执行相应的命令。

    注意:bash中的复制粘贴操作与linux中类似,ctrl+C用于终止进程,可以用鼠标中键进行粘贴操作。不嫌麻烦的话可以使用ctrl+shift+Cctrl+shift+V进行复制粘贴操作。

  3. 安装hexo

    hexo是一个快速、简洁且高效的博客框架,在这里我们使用hexo来搭建博客。
    首先,新建一个名为“blog”的空文件夹,以后我们的操作都在这个文件夹里进行,可以在bash中使用pwd命令查看当前所处位置。
    创建这个文件夹的目的是万一因为创建的博客出现问题或者不满意想重来等原因可以直接简单地把文件夹删除,也方便了对整个网站本地内容的移动。
    打开新建的文件夹,右键空白处,选择Git Bash Here。 接下来我们输入两行命令来验证node.js是否安装成功。 如出现如图所示结果,则表明安装成功。
    为了提高以后的下载速度,我们需要安装cnpm。cnpm是淘宝的国内镜像,因为npm的服务器位于国外有时可能会影响安装。
    继续在bash中输入npm install -g cnpm --registry=https://registry.npm.taobao.org安装cnpm。
    检验安装是否成功,输入cnpm
    如果输出结果显示如下,说明安装成功。 接下来我们安装hexo,输入命令cnpm install -g hexo-cli
    和上面一样,我们可以用hexo -v来验证是否成功安装hexo,这里就不贴图了。
    接下来我们输入hexo init来初始化建立整个项目。
    你会发现你的文件夹中多了许多文件,你也可以用ls -l命令来看到新增的文件。
  4. 完成本地环境的搭建

    至此,我们已经完成了本地环境的搭建,在这里,我想先介绍hexo中常用的命令。
    hexo n "文章标题":用于创建新的博文(欲删除文章,直接删除md文件并用下面的命令更新即可)。
    hexo s也就是hexo start:hexo会监视文件变动并自动更新,通过所给的地址localhost:4000/就可以直接在本地预览更新后的网站了。这里不需要generate,保存修改后的本地文件后可以直接看到效果。
    部署到远端服务器三步曲:

    1
    2
    3
    hexo clean #清除缓存,网页正常情况下可以忽略此条命令,执行该指令后,会删掉站点根目录下的public文件夹。
    hexo g #generate静态网页(静态网页这里指没有前端后端的网页而不是静止),该命令把md编译为html并存到public文件目录下。
    hexo d #将本地的更改部署到远端服务器(需要一点时间,请过一会再刷新网页)。

    此外,上面最后两步也可以使用hexo g -d直接代替。
    如果出现ERROR Deployer not found: git报错,可以使用npm install --save hexo-deployer-git命令解决。

    注意:由于部署到远端输入密码时密码不可见,有时候会导致部署失败,只有出现INFO Deploy done: git的结果才表明部署成功,否则再次部署重输密码即可。

    现在我们在bash中运行hexo s,打开浏览器,输入localhost:4000/,就可以看到hexo默认创建的页面了。

  5. 部署到远端服务器

    为了让别人能访问到你搭建的网站,我们需要部署到远端服务器。
    这里有两种选择,一种是部署到github上,新建一个repository,然后创建一个xxxxx.github.io域名(这里xxxxx必须为你的github用户名)。
    另一种选择是部署到国内的coding,这是考虑到访问速度的问题,不过我选择的是前者,亲测并没感觉有速度的困扰。
    个人比较推荐用github pages创建个人博客。
    部署这块网上有许多教程,这里不详细解释了,以后有机会补上。
    在部署的时候涉及到对主题配置文件的操作,linux和mac用户可以使用vim进行编辑,不过也可以使用VScode、sublime等代码编辑器进行操作。

    注:为了国内的访问速度,我最后添加了coding/github双线部署,两者的操作方式大同小异。值得注意的是,如果使用的是leancloud的第三方阅读量与评论统计系统,那么还得在leancloud的安全中心中添加coding的web域名。此外,部署到远端之后(特别是第一次)可能要等待几分钟才能访问到更新之后的页面。


创建原因

首先说明,我只是一个刚起步的很萌的萌新,懂得不多,别喷我哈~
步入大二,虽然我是大学才算真正接触编程,但一年多下来我也接触并且学习了不少技术知识。接触的多了、遇到的问题也复杂了起来,导致每次百度到的答案不一定能够解决我遇到的问题。此外,之前在学习编程语言、操作系统、ML、DL等知识的时候,为方便起见利用文本记了些笔记。然而笔记分散在四处,不方便管理与查看,因此就萌生了写博客的想法。由于个人比较喜欢自由DIY,所以没有使用CSDN、博客园等知名技术博客网站。
最后还是非常感谢我们华科的校友程序羊在b站和其他站点上分享的各种经验,我就是通过他的视频来搭建起自己的第一个博客网站的。他的其他视频也给了我很多启迪。
最后,最关键的原因,还是因为今天中秋节有空闲的时间哈哈,祝大家节日快乐!


碰到底线咯 后面没有啦

本文标题:hexo笔记:开始创建个人博客——方法及原因

文章作者:高深远

发布时间:2019年09月13日 - 15:33

最后更新:2020年02月27日 - 07:48

原始链接:https://gsy00517.github.io/hexo20190913153310/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:在linux(ubuntu)下安装使用 | 高深远的博客

hexo笔记:在linux(ubuntu)下安装使用

之前一直在win10下使用hexo搭建部署博客,方法参见:hexo笔记:开始创建个人博客——方法及原因。那么,如果想在linux环境下使用hexo,该如何操作呢?

References

电子文献:
https://blog.csdn.net/y5492853/article/details/79529410


原因

由于在更改主题配置文件_config.yml时,长达八百多行的配置文件总是让我找得头晕目眩。由于VScode(好像)没有提供字符的快速查找匹配功能,我之前一直采用一种笨拙的办法,即在文件的空处输入想要查找的字符然后左键选中,这个时候文件中同样的字符也会被选中,这样快速拉动滚动条时就可以比较明显地发现想找的目标字符了。
然而对于这种办法,我觉得主要有两大问题:

  1. 忘记删除在空白处添加的文字

    我就犯过这样低级的错误,找到并更改之后没有删除自己添加的字符就直接快乐地ctrl+S了,于是就造成了网站能打开但是一片空白的bug。所以大家没事还是不要随意在主题配置文件中添加文字。
  2. 不是长久之计

    虽然这个八百多行的文件已经让我够呛了,然后或许今后还会遇到更长的文件,那么这种方法就会变得极其低效(而且伤眼睛)。
    基于这些因素,我脑子里的第一个反映就是vim编辑器中的对文件字符的查找定位的功能(关于vim的使用,等我多多尝试并熟练之后再做小结)。
    好了,接下来就开始操作吧。

更正

最近突然发现VScode自带了搜索功能,可以直接在整个文件夹中搜索关键词。这里所给的快捷键是ctrl+shift+F,但win10用户可能会发现按了之后没有任何反应。事实上,反应还是有的,当你再次打字时,就会发现简体变成了繁体,再次按ctrl+shift+F即可恢复。

直接点击搜索图标即可便捷地进行搜索,为我之前眼瞎没有发现表示无奈,但下面还是写一下怎么安装。


安装

首先安装node.js。这里就没windows下直接双击exe安装包那么easy啦,打开终端,老老实实输命令:

1
2
3
sudo apt-get install nodejs
sudo apt install nodejs-legacy
sudo apt install npm

其实熟练之后觉得apt是真的好用。
由于ubuntu源中的node.js是旧版本,下面会出现问题,我在后文解释。
由于npm服务器在国外可能会影响下载速度,和windows下的步骤一样,我们换成淘宝镜像:

1
sudo npm config set registry https://registry.npm.taobao.org

这时候如果我们直接安装hexo,会出现如下错误:

因此,我们安装node升级工具n:

1
sudo npm install n -g

并且使用sudo n stable升级版本,若看到如下输出,说明升级成功:

注意:fetch可能需要花费一点时间,这时候终端不会有任何输出,不要以为出错了,耐心等待即可,不要ctrl+C中止。

最后,我们安装hexo:

1
sudo npm install -g hexo

注:-g表示安装到全局环境。

接下来的初始化操作跟windows下基本一样,可以参照我之前的博文。我继续对原来的博客进行编辑,所以无需初始化一个新的,直接把windows的对应文件夹整个copy过来就行了。


碰到底线咯 后面没有啦

本文标题:hexo笔记:在linux(ubuntu)下安装使用

文章作者:高深远

发布时间:2019年09月17日 - 08:56

最后更新:2020年02月07日 - 17:02

原始链接:https://gsy00517.github.io/hexo20190917085649/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:在linux(ubuntu)下安装使用 | 高深远的博客

hexo笔记:在linux(ubuntu)下安装使用

之前一直在win10下使用hexo搭建部署博客,方法参见:hexo笔记:开始创建个人博客——方法及原因。那么,如果想在linux环境下使用hexo,该如何操作呢?

References

电子文献:
https://blog.csdn.net/y5492853/article/details/79529410


原因

由于在更改主题配置文件_config.yml时,长达八百多行的配置文件总是让我找得头晕目眩。由于VScode(好像)没有提供字符的快速查找匹配功能,我之前一直采用一种笨拙的办法,即在文件的空处输入想要查找的字符然后左键选中,这个时候文件中同样的字符也会被选中,这样快速拉动滚动条时就可以比较明显地发现想找的目标字符了。
然而对于这种办法,我觉得主要有两大问题:

  1. 忘记删除在空白处添加的文字

    我就犯过这样低级的错误,找到并更改之后没有删除自己添加的字符就直接快乐地ctrl+S了,于是就造成了网站能打开但是一片空白的bug。所以大家没事还是不要随意在主题配置文件中添加文字。
  2. 不是长久之计

    虽然这个八百多行的文件已经让我够呛了,然后或许今后还会遇到更长的文件,那么这种方法就会变得极其低效(而且伤眼睛)。
    基于这些因素,我脑子里的第一个反映就是vim编辑器中的对文件字符的查找定位的功能(关于vim的使用,等我多多尝试并熟练之后再做小结)。
    好了,接下来就开始操作吧。

更正

最近突然发现VScode自带了搜索功能,可以直接在整个文件夹中搜索关键词。这里所给的快捷键是ctrl+shift+F,但win10用户可能会发现按了之后没有任何反应。事实上,反应还是有的,当你再次打字时,就会发现简体变成了繁体,再次按ctrl+shift+F即可恢复。

直接点击搜索图标即可便捷地进行搜索,为我之前眼瞎没有发现表示无奈,但下面还是写一下怎么安装。


安装

首先安装node.js。这里就没windows下直接双击exe安装包那么easy啦,打开终端,老老实实输命令:

1
2
3
sudo apt-get install nodejs
sudo apt install nodejs-legacy
sudo apt install npm

其实熟练之后觉得apt是真的好用。
由于ubuntu源中的node.js是旧版本,下面会出现问题,我在后文解释。
由于npm服务器在国外可能会影响下载速度,和windows下的步骤一样,我们换成淘宝镜像:

1
sudo npm config set registry https://registry.npm.taobao.org

这时候如果我们直接安装hexo,会出现如下错误:

因此,我们安装node升级工具n:

1
sudo npm install n -g

并且使用sudo n stable升级版本,若看到如下输出,说明升级成功:

注意:fetch可能需要花费一点时间,这时候终端不会有任何输出,不要以为出错了,耐心等待即可,不要ctrl+C中止。

最后,我们安装hexo:

1
sudo npm install -g hexo

注:-g表示安装到全局环境。

接下来的初始化操作跟windows下基本一样,可以参照我之前的博文。我继续对原来的博客进行编辑,所以无需初始化一个新的,直接把windows的对应文件夹整个copy过来就行了。


碰到底线咯 后面没有啦

本文标题:hexo笔记:在linux(ubuntu)下安装使用

文章作者:高深远

发布时间:2019年09月17日 - 08:56

最后更新:2020年02月07日 - 17:02

原始链接:https://gsy00517.github.io/hexo20190917085649/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:ssh与https以及双线部署的一些注意点 | 高深远的博客

hexo笔记:ssh与https以及双线部署的一些注意点

不久前,我对本博客网站做了一些优化,其中包括将网站同时部署到github和coding上。关于双线部署如何具体操作,网上有许多较为详尽的教程可以参考,如果有问题的话可以参考多篇不同的教程找出原因解决。在这篇文章中,我主要想讲讲我这期间遇到的一些小事项。


github与coding

考虑到每次打开博客的加载速度问题,我前几天尝试了把博客部署到coding上,实现了coding+github双线部署。coding现已经被腾讯云收购,可以直接用微信登录。
部署完成后,为了看一看效果,我使用了站长工具分别对coding和github上的网站速度进行了测试。测试结果如下:


可见,部署在coding上确实能提高一点速度。不过事实上,在实际使用中,并没有感到coding更快,搜索之后发现似乎是coding的服务器也不在内地而在香港的原因。由于coding总是会更改服务和功能,其稳定性远远不如github。不过,coding page的确能被百度更快地爬取,更新的文章能够很快地被收录。这里附上我的两个链接,可以看看效果,择优访问:
github page:https://gsy00517.github.io/
coding page:https://gsy00517.coding.me/

注意:由于版本更新,coding page现已无法正常访问,详见欢迎到访:写在前面


ssh与https

在网上的一个教程中,作者提到使用ssh比https更加稳定,尝试后暂时没有发现明显的区别,但是另一个直观的改变就是在push代码时,使用ssh url就不需要输入账号和密码。下面是我在hexo配置文件中的设置,也就是位于站点根目录下的_config.yml文件,其中后面注释中的https://github.com/Gsy00517/Gsy00517.github.io.git是原本的https url。

上面对应的ssh url一般可以从平台上直接复制获取,也可以参照我的格式进行设置。

这里简要说一说ssh与https的区别。
一般默认情况下使用的是https,除了需要在fetch和push时使用密码之外,使用https的配置比较方便。然而,使用ssh url却需要先配置和添加好ssh key,并且你必须是这个项目的拥有或者管理者,而https就没有这些要求。其实,配置ssh key也并没有那么繁琐,而且这是一劳永逸的,所以推荐还是使用ssh。
要注意的是,ssh key保存的默认位置或许会不同于网上的教程,不过可以自行更改。我的默认地址是在用户文件夹下的AppData\Roaming\SPB_16.6的ssh文件夹中。AppData文件夹默认是隐藏的,可以通过查看隐藏的项目打开。此外,如果需要经常清理temp文件的话,不妨取消这个文件夹的隐藏,这在释放windows空间中还是挺有效的,可以参见windows笔记:释放空间

key所在的文件是上图所示的第二个publisher文件,然而似乎无法直接用office打开,选择打开方式为记事本即可。
当然,如果实在找不到key所在的文件,也可以直接使用文件资源管理器的搜索功能查找名为.ssh的文件夹即可。

注:http与https的区别在于,http是明文传输的,而https是使用ssl加密的,更加安全。若要将连接提交百度站点验证,就需要使用https协议,这个在github和coding都有强制https访问的选项。


双线部署注意事项

  1. LeanCloud

    这里主要针对hexo博客双线部署后可能会出现的几个问题说明一下注意点。
    首先,如果之前使用的是LeanCloud来接受记录评论和统计阅读量的,那么为了共享数据,必须在LeanCloud控制台设置的安全中心中,添加新增的web安全域名,保存后即可解决问题。
  2. Widget

    如果使用的是基于Widget的评分系统,那么必须更改Widget设置中的domain。我是免费使用Widget,只能同时添加一个domain。我继续使用github page的域名,因此只能在我的github page中看到评分系统。
  3. 文内链接

    因为双线部署用的依旧还是同一份本地源码文件,因此在博文中提供的链接依旧是一致的。这里我也将继续使用github page的链接,也就是文内推荐的我本人的博文链接依旧还是指向github page的。事实上,这并无任何影响。

碰到底线咯 后面没有啦

本文标题:hexo笔记:ssh与https以及双线部署的一些注意点

文章作者:高深远

发布时间:2019年10月06日 - 23:27

最后更新:2020年03月10日 - 18:00

原始链接:https://gsy00517.github.io/hexo20191006232704/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:ssh与https以及双线部署的一些注意点 | 高深远的博客

hexo笔记:ssh与https以及双线部署的一些注意点

不久前,我对本博客网站做了一些优化,其中包括将网站同时部署到github和coding上。关于双线部署如何具体操作,网上有许多较为详尽的教程可以参考,如果有问题的话可以参考多篇不同的教程找出原因解决。在这篇文章中,我主要想讲讲我这期间遇到的一些小事项。


github与coding

考虑到每次打开博客的加载速度问题,我前几天尝试了把博客部署到coding上,实现了coding+github双线部署。coding现已经被腾讯云收购,可以直接用微信登录。
部署完成后,为了看一看效果,我使用了站长工具分别对coding和github上的网站速度进行了测试。测试结果如下:


可见,部署在coding上确实能提高一点速度。不过事实上,在实际使用中,并没有感到coding更快,搜索之后发现似乎是coding的服务器也不在内地而在香港的原因。由于coding总是会更改服务和功能,其稳定性远远不如github。不过,coding page的确能被百度更快地爬取,更新的文章能够很快地被收录。这里附上我的两个链接,可以看看效果,择优访问:
github page:https://gsy00517.github.io/
coding page:https://gsy00517.coding.me/

注意:由于版本更新,coding page现已无法正常访问,详见欢迎到访:写在前面


ssh与https

在网上的一个教程中,作者提到使用ssh比https更加稳定,尝试后暂时没有发现明显的区别,但是另一个直观的改变就是在push代码时,使用ssh url就不需要输入账号和密码。下面是我在hexo配置文件中的设置,也就是位于站点根目录下的_config.yml文件,其中后面注释中的https://github.com/Gsy00517/Gsy00517.github.io.git是原本的https url。

上面对应的ssh url一般可以从平台上直接复制获取,也可以参照我的格式进行设置。

这里简要说一说ssh与https的区别。
一般默认情况下使用的是https,除了需要在fetch和push时使用密码之外,使用https的配置比较方便。然而,使用ssh url却需要先配置和添加好ssh key,并且你必须是这个项目的拥有或者管理者,而https就没有这些要求。其实,配置ssh key也并没有那么繁琐,而且这是一劳永逸的,所以推荐还是使用ssh。
要注意的是,ssh key保存的默认位置或许会不同于网上的教程,不过可以自行更改。我的默认地址是在用户文件夹下的AppData\Roaming\SPB_16.6的ssh文件夹中。AppData文件夹默认是隐藏的,可以通过查看隐藏的项目打开。此外,如果需要经常清理temp文件的话,不妨取消这个文件夹的隐藏,这在释放windows空间中还是挺有效的,可以参见windows笔记:释放空间

key所在的文件是上图所示的第二个publisher文件,然而似乎无法直接用office打开,选择打开方式为记事本即可。
当然,如果实在找不到key所在的文件,也可以直接使用文件资源管理器的搜索功能查找名为.ssh的文件夹即可。

注:http与https的区别在于,http是明文传输的,而https是使用ssl加密的,更加安全。若要将连接提交百度站点验证,就需要使用https协议,这个在github和coding都有强制https访问的选项。


双线部署注意事项

  1. LeanCloud

    这里主要针对hexo博客双线部署后可能会出现的几个问题说明一下注意点。
    首先,如果之前使用的是LeanCloud来接受记录评论和统计阅读量的,那么为了共享数据,必须在LeanCloud控制台设置的安全中心中,添加新增的web安全域名,保存后即可解决问题。
  2. Widget

    如果使用的是基于Widget的评分系统,那么必须更改Widget设置中的domain。我是免费使用Widget,只能同时添加一个domain。我继续使用github page的域名,因此只能在我的github page中看到评分系统。
  3. 文内链接

    因为双线部署用的依旧还是同一份本地源码文件,因此在博文中提供的链接依旧是一致的。这里我也将继续使用github page的链接,也就是文内推荐的我本人的博文链接依旧还是指向github page的。事实上,这并无任何影响。

碰到底线咯 后面没有啦

本文标题:hexo笔记:ssh与https以及双线部署的一些注意点

文章作者:高深远

发布时间:2019年10月06日 - 23:27

最后更新:2020年03月10日 - 18:00

原始链接:https://gsy00517.github.io/hexo20191006232704/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:SEO优化 | 高深远的博客

hexo笔记:SEO优化

toefl笔记:首考托福——记一次裸考经历文章末尾我曾提到在被百度收录之后要好好做SEO,这段时间我也的确有所尝试与改进,因此在本文中将一些我认为比较有效的或者依旧存疑的SEO优化方法写下来,供日后参考深究。

References

电子文献:
https://blog.csdn.net/lzy98/article/details/81140704
https://www.jianshu.com/p/86557c34b671
https://baijiahao.baidu.com/s?id=1616368344109675728&wfr=spider&for=pc
https://www.jianshu.com/p/7e1166eb412a
https://baijiahao.baidu.com/s?id=1597172076743185609&wfr=spider&for=pc


SEO

之前在知乎上碰巧看到一篇别人是如何推广自己的博客的文章,里面就提到了SEO这个概念。我当时也很好奇,百度之后才发现它完全不同于CEO、CTO等概念。SEO(Search Engine Optimization),汉译为搜索引擎优化。它是一种方式,即利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。通俗的讲就是post的内容更容易被搜索引擎搜索到或者收录,且在搜索结果列表中显示靠前。
看了一圈,SEO的办法真的是多种多样,下面我就简单记录一部分我试过的方法。


优化url

同样在站点配置文件下面,可以找到站点的url设置。
如果你尚未更改过,你会发现默认的url是http://yoursite.com,我在这里吃了不少亏,之前苹果上add to favorites、RSS订阅后点开的链接以及copyright的链接都会直接跳转到yoursite而非我的博文链接。
SEO搜索引擎优化认为,网站的最佳结构是用户从首页点击三次就可以到达任何一个页面,但是我们使用hexo编译的站点默认打开文章的url是“sitename/year/mounth/day/title”四层的结构,这样的url结构很不利于SEO,爬虫就会经常爬不到我们的文章,于是,我们可以将url直接改成“sitename/title”的形式,并且title最好是用英文(中文的url会出现好多乱码,我这方面还有待改进)。
基于以上原因,我在根目录的站点配置文件下修改url设置如下(注释中是默认的):

如此,再次添加RSS订阅,就可以跟yoursite这个鬼地方say goodbye啦。
对permalink的修改将会是你的站点的一次巨大的变动,会造成大量的死链。死链会造成搜索引擎对网站的评分降低并有可能会降权。我们可以直接百度搜索“site:url”(url即你的站点网址)查看已经被搜索引擎收录的网址。如下图所示,当时我被收录了四个(截止本文最近更新前收录已达60多),其中前两个经此番调整已成为死链。


这时我们可以在百度站长平台中提交死链,由于死链文件制作稍较复杂,我们可以选择规则提交的方式提交死链(处理死链过程较长,我提交的死链目前还在处理中)。
很重要的是,我们需要在自己的所有博文中修改链接,我使用了VScode的搜索关键字符功能对所有markdown文件进行了修改,效率相对较高。此外,如果使用了leancloud等第三方服务,那么也需要修改对应的url与新的相匹配,否则会造成原来数据的丢弃,还是挺可惜的。


压缩文件

关于压缩的方法,网上有很多,可以选择简易的应用。我选择的是用hexo-neat,安装插件后在站点配置文件添加如下设置,效果不错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# hexo-neat
# 博文压缩
neat_enable: true
# 压缩html
neat_html:
enable: true
exclude:
# 压缩css
neat_css:
enable: true
exclude:
- '**/*.min.css'
# 压缩js
neat_js:
enable: true
mangle: true
output:
compress:
exclude:
- '**/*.min.js'
- '**/jquery.fancybox.pack.js'
- '**/index.js'

添加完成之后,每次generate你就会在git bash终端看到neat压缩的反馈信息。
另外也有和很多网友使用的是gulp压缩,设置也很简便且有效。
压缩网站文件不仅可以提高访问加载的速度,同时减少了大量空白符,对SEO也是有不小的帮助的,推荐尝试。


主动推送

首先在根目录下安装插件npm install hexo-baidu-url-submit --save
在根目录站点配置文件中新增如下字段:

1
2
3
4
5
baidu_url_submit:
count: 100 # 提交最新的一个链接
host: gsy00517.github.io # 在百度站长平台中注册的域名
token: lY..........Fk # 请注意这是您的秘钥,所以请不要把博客源代码发布在公众仓库里!
path: baidu_urls.txt # 文本文档的地址,新链接会保存在此文本文档里

域名和秘钥可以在站长工具平台的连接提交中的接口调用地址中找到,即对应host与token后面的字段。
再把主题配置文件中的deploy修改如下:

1
2
3
4
5
6
7
8
9
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
- type: git
repo:
github: git@github.com:Gsy00517/Gsy00517.github.io.git
coding: git@git.dev.tencent.com:gsy00517/gsy00517.git
branch: master
- type: baidu_url_submitter

注意:必须严格按照上述格式,否则无法deploy。

这样以后每次执行hexo d,新的链接就会主动推送给百度,然后百度就会更快地派爬虫来发现你站点中的新链接,可以在第一时间收录新建的链接。


自动推送

自动推送是百度搜索资源平台为提高站点新增网页发现速度推出的工具,安装自动推送JS代码的网页,在页面被访问时,页面url将立即被推送给百度。详情可以查看百度的站长工具平台使用帮助。事实上,如果已经实行了主动推送,那么自动推送其实不是那么必要,因为主动推送是在生成url的时候第一时间进行推送,之后访问页面时进行的自动推送就显得晚了一步。不同推送方式的效果大概是:主动推送>自动推送>sitemap。
下面还是写一下自动推送的实现方法。
blog\themes\next\source\js\src目录下,创建名为bai.js的文件,并根据百度提供的自动推送功能方法添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
(function(){
var bp = document.createElement('script');
var curProtocol = window.location.protocol.split(':')[0];
if (curProtocol === 'https') {
bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
}
else {
bp.src = 'http://push.zhanzhang.baidu.com/push.js';
}
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();
</script>

此外,还可以blog\scaffolds目录下的模板文件post.md的分隔线之后添加这么一行:

1
<script type="text/javascript" src="/js/src/bai.js"></script>

这样以后每次创建新的文章就会自动在文末添加这行代码,即在生成的模板中包含这行代码。
如此,只要访问你的这个页面,它就会自动向百度推送你的这个网页。


sitemap

首先需要安装sitemap站点地图自动生成插件。
windows下打开git bash,输入安装命令:

1
2
npm install hexo-generator-sitemap --save
npm install hexo-generator-baidu-sitemap --save

然后在站点配置文件_config.yml中找到如下对应的位置(一般默认有,没有的话可以添加),修改如下:

1
2
3
4
5
# 自动生成sitemap
sitemap:
path: sitemap.xml
baidusitemap:
path: baidusitemap.xml

特别要注意的是,上面的path一定要缩进,否则在hexo generate时会无法编译导致报错。(似乎有一些版本的hexo不存在这样的问题,关于版本可以使用hexo version命令查看)
这样以后每次generate后都会在public目录下面生成sitemap.xml和baidusitemap.xml两个文件,即你的站点地图。也可以deploy后直接在域名后面加上这两个文件名查看你的站点地图。
在百度站长平台中,有sitemap提交的选项,由于我当初提交的网站协议前缀是http,因此xml文件所在的https前缀的链接不属于我提交的网站,而我的github page和coding page都设置了强制https访问。这个问题以后有机会再做解决,不存在这个问题的可以试试提交sitemap。


疑惑

在优化的过程中,我发现我的post模板也被改变了(原因目前未知),从原本的:

1
2
3
4
5
title: {{ title }}
date: {{ date }}
tags:
copyright: true
top:

变成了:

1
2
3
4
5
6
7
8
9
10
11
---
noteId: "9bfafbb............dd5a3"
tags: []
title:
[object Object]: null
date:
[object Object]: null
copyright: true
top: null

---

更奇怪的是,我无法删除noteId并恢复到原来的样式,每次更改保存之后又会自动给我换回来,为了方便使用,我将其修改为:

1
2
3
4
5
6
7
8
9
10
11
---
noteId: "55c6d..............d6fcf"
title:
{{ title }}
date:
{{ date }}
copyright: true
top:
categories:
tags:
---

这样就可以直接提取文章标题和创建时间了。
对于noteId的作用,网上也找不到相关信息,可能是类似于网站的ID标识的一个代号吧,我对它之后的改进以及用法可见后文。


使用noteId改进url

今天看了几个url中含有noteId的网站,立马想到其实noteId其实可以用来替代url的中文等符号从而消除乱码,这更方便了爬虫的抓取。于是,我把站点配置文件下的url设置修改如下:

同时我把模板文件post.md修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
---
noteId: 'prefix+time remember to change!!!'
title:
{{ title }}
date:
{{ date }}
copyright: true
top:
categories:
tags:
---

<script type="text/javascript" src="/js/src/bai.js"></script>

这就把我的博文网址修改成了“关键词+创建时间”的形式,当然要手动更改。
同样的,以上的操作也带来了巨大的麻烦。我需要给之前没有生成noteId的博文一一加上noteId,同时也免不了对外部辅助平台和网站内链的大幅度修改。
对于检查网站死链,我推荐一个挺实用的轻量工具Xenu,下载安装之后,选择file,然后check URL,输入网站地址,即可检查站内所有的连接中是否存在死链。下面是我仅修改了url设置而未更改内链时检测的情况,其中红色的就是死链。


修改url之后

大约是在我修改了url格式的两天后,当我再用“site:url”查询收录情况时,我发现被收录的死链已经减少了一个(似乎不是提交死链的原因,因为规则提交的死链还在处理中),然而我之前被收录、修改后原url依然可用的主页和分类页面却也消失了,这就使我非常得纳闷。这几日也查找了许多资料寻找原因,总结如下。

首先,网站url的变动产生大量死链,很有可能会导致网站排名消失,原来积累的权重大大减少甚至清除。还好目前我只是一个新站,倘若已运行并被收录了一段时间,应该要慎重考虑是否是因为网址必须得精简等原因从而放弃网站的排名。要注意的是,如果网站url链接过深,会影响搜索引擎蜘蛛抓取,时间久了,蜘蛛来的次数就会减少,最后导致网站不收录。一般建议扁平化结构,url在三层以内方便蜘蛛爬行,这在上文也提到过。
此外,如果是新站的话,收录之后消失也是正常的。事实上,上线6个月之内的网站都可以被称为新站。因为搜索引擎蜘蛛对新站有一个好奇心,发现新鲜的事物喜欢去抓取一下,这就是收录,但是收录之后会有一个审核期,包括这个收录之后又消失的问题,审核期过后如果在数据库找不到相同的信息就会认为这是一篇原创,这个时候再去看收录就又会恢复了。值得注意的是,新站上线短期内,只新增更新内容就行了,不要去改动以前的内容(特别是标题、url等,搜索引擎对这些内容很敏感)以免延长新站考核时间,当网站索引趋于稳定状态后可以适当改动。
总而言之,目前没什么好担心的,担心也没有用,还是认认真真好好地写笔记好啦!


添加robots文件

Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站可以通过Robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。
如果一个网站使用大量的js、flash、ifrmae等内容,或者如果一个网站结构混乱,那么整个网站就会是乱七八糟、毫无章法,不仅用户体验极差,更重要的是蜘蛛也不会喜欢,也没有心思去抓取网站的内容了。
robots.txt是搜索引擎蜘蛛访问网站时要查看的第一个文件,并且会根据robots.txt文件的内容来爬行网站。在某种意义上说,它的一个任务就是指导蜘蛛爬行,减少搜索引擎蜘蛛的工作量。
当搜索引擎蜘蛛访问网站时,它会首先检查该站点根目录下是否存在robots.txt文件,如果该文件存在,搜索引擎蜘蛛就会按照该文件中的内容来确定爬行的范围;如果该文件不存在,则所有的搜索引擎蜘蛛将能够访问网站上所有没有被口令保护的页面。
通常搜索引擎对网站派出的蜘蛛是有配额的,多大规模的网站放出多少蜘蛛。如果我们不配置robots文件,那么蜘蛛来到网站以后会无目的地爬行,造成的一个结果就是,需要它爬行的目录,没有爬行到,不需要爬行的,也就是我们不想被收录的内容却被爬行并放出快照。所以robots文件对于SEO具有重要的意义。
如果网站中没有robots.txt文件,则网站中的程序脚本、样式表等一些和网站内容无关的文件或目录即使被搜索引擎蜘蛛爬行,也不会增加网站的收录率和权重,只会浪费服务器资源。此外,搜索引擎派出的蜘蛛资源也是有限的,我们要做的应该是尽量让蜘蛛爬行网站重点文件、目录,最大限度的节约蜘蛛资源。
在站点根目录的source文件下添加robots.txt文件,加入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
User-agent: * 
Allow: /
Allow: /archives/
Disallow: /categories/
Disallow: /tags/
Disallow: /vendors/
Disallow: /js/
Disallow: /css/
Disallow: /fonts/
Disallow: /vendors/
Disallow: /fancybox/

Sitemap: https://gsy00517.github.io/sitemap.xml
Sitemap: https://gsy00517.github.io/baidusitemap.xml

注意sitemap中要修改成自己的url。
另外,可以在站长工具平台检测robots文件。


碰到底线咯 后面没有啦

本文标题:hexo笔记:SEO优化

文章作者:高深远

发布时间:2019年11月01日 - 21:20

最后更新:2020年02月07日 - 17:19

原始链接:https://gsy00517.github.io/hexo20191101212014/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:SEO优化 | 高深远的博客

hexo笔记:SEO优化

toefl笔记:首考托福——记一次裸考经历文章末尾我曾提到在被百度收录之后要好好做SEO,这段时间我也的确有所尝试与改进,因此在本文中将一些我认为比较有效的或者依旧存疑的SEO优化方法写下来,供日后参考深究。

References

电子文献:
https://blog.csdn.net/lzy98/article/details/81140704
https://www.jianshu.com/p/86557c34b671
https://baijiahao.baidu.com/s?id=1616368344109675728&wfr=spider&for=pc
https://www.jianshu.com/p/7e1166eb412a
https://baijiahao.baidu.com/s?id=1597172076743185609&wfr=spider&for=pc


SEO

之前在知乎上碰巧看到一篇别人是如何推广自己的博客的文章,里面就提到了SEO这个概念。我当时也很好奇,百度之后才发现它完全不同于CEO、CTO等概念。SEO(Search Engine Optimization),汉译为搜索引擎优化。它是一种方式,即利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。通俗的讲就是post的内容更容易被搜索引擎搜索到或者收录,且在搜索结果列表中显示靠前。
看了一圈,SEO的办法真的是多种多样,下面我就简单记录一部分我试过的方法。


优化url

同样在站点配置文件下面,可以找到站点的url设置。
如果你尚未更改过,你会发现默认的url是http://yoursite.com,我在这里吃了不少亏,之前苹果上add to favorites、RSS订阅后点开的链接以及copyright的链接都会直接跳转到yoursite而非我的博文链接。
SEO搜索引擎优化认为,网站的最佳结构是用户从首页点击三次就可以到达任何一个页面,但是我们使用hexo编译的站点默认打开文章的url是“sitename/year/mounth/day/title”四层的结构,这样的url结构很不利于SEO,爬虫就会经常爬不到我们的文章,于是,我们可以将url直接改成“sitename/title”的形式,并且title最好是用英文(中文的url会出现好多乱码,我这方面还有待改进)。
基于以上原因,我在根目录的站点配置文件下修改url设置如下(注释中是默认的):

如此,再次添加RSS订阅,就可以跟yoursite这个鬼地方say goodbye啦。
对permalink的修改将会是你的站点的一次巨大的变动,会造成大量的死链。死链会造成搜索引擎对网站的评分降低并有可能会降权。我们可以直接百度搜索“site:url”(url即你的站点网址)查看已经被搜索引擎收录的网址。如下图所示,当时我被收录了四个(截止本文最近更新前收录已达60多),其中前两个经此番调整已成为死链。


这时我们可以在百度站长平台中提交死链,由于死链文件制作稍较复杂,我们可以选择规则提交的方式提交死链(处理死链过程较长,我提交的死链目前还在处理中)。
很重要的是,我们需要在自己的所有博文中修改链接,我使用了VScode的搜索关键字符功能对所有markdown文件进行了修改,效率相对较高。此外,如果使用了leancloud等第三方服务,那么也需要修改对应的url与新的相匹配,否则会造成原来数据的丢弃,还是挺可惜的。


压缩文件

关于压缩的方法,网上有很多,可以选择简易的应用。我选择的是用hexo-neat,安装插件后在站点配置文件添加如下设置,效果不错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# hexo-neat
# 博文压缩
neat_enable: true
# 压缩html
neat_html:
enable: true
exclude:
# 压缩css
neat_css:
enable: true
exclude:
- '**/*.min.css'
# 压缩js
neat_js:
enable: true
mangle: true
output:
compress:
exclude:
- '**/*.min.js'
- '**/jquery.fancybox.pack.js'
- '**/index.js'

添加完成之后,每次generate你就会在git bash终端看到neat压缩的反馈信息。
另外也有和很多网友使用的是gulp压缩,设置也很简便且有效。
压缩网站文件不仅可以提高访问加载的速度,同时减少了大量空白符,对SEO也是有不小的帮助的,推荐尝试。


主动推送

首先在根目录下安装插件npm install hexo-baidu-url-submit --save
在根目录站点配置文件中新增如下字段:

1
2
3
4
5
baidu_url_submit:
count: 100 # 提交最新的一个链接
host: gsy00517.github.io # 在百度站长平台中注册的域名
token: lY..........Fk # 请注意这是您的秘钥,所以请不要把博客源代码发布在公众仓库里!
path: baidu_urls.txt # 文本文档的地址,新链接会保存在此文本文档里

域名和秘钥可以在站长工具平台的连接提交中的接口调用地址中找到,即对应host与token后面的字段。
再把主题配置文件中的deploy修改如下:

1
2
3
4
5
6
7
8
9
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
- type: git
repo:
github: git@github.com:Gsy00517/Gsy00517.github.io.git
coding: git@git.dev.tencent.com:gsy00517/gsy00517.git
branch: master
- type: baidu_url_submitter

注意:必须严格按照上述格式,否则无法deploy。

这样以后每次执行hexo d,新的链接就会主动推送给百度,然后百度就会更快地派爬虫来发现你站点中的新链接,可以在第一时间收录新建的链接。


自动推送

自动推送是百度搜索资源平台为提高站点新增网页发现速度推出的工具,安装自动推送JS代码的网页,在页面被访问时,页面url将立即被推送给百度。详情可以查看百度的站长工具平台使用帮助。事实上,如果已经实行了主动推送,那么自动推送其实不是那么必要,因为主动推送是在生成url的时候第一时间进行推送,之后访问页面时进行的自动推送就显得晚了一步。不同推送方式的效果大概是:主动推送>自动推送>sitemap。
下面还是写一下自动推送的实现方法。
blog\themes\next\source\js\src目录下,创建名为bai.js的文件,并根据百度提供的自动推送功能方法添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
(function(){
var bp = document.createElement('script');
var curProtocol = window.location.protocol.split(':')[0];
if (curProtocol === 'https') {
bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
}
else {
bp.src = 'http://push.zhanzhang.baidu.com/push.js';
}
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();
</script>

此外,还可以blog\scaffolds目录下的模板文件post.md的分隔线之后添加这么一行:

1
<script type="text/javascript" src="/js/src/bai.js"></script>

这样以后每次创建新的文章就会自动在文末添加这行代码,即在生成的模板中包含这行代码。
如此,只要访问你的这个页面,它就会自动向百度推送你的这个网页。


sitemap

首先需要安装sitemap站点地图自动生成插件。
windows下打开git bash,输入安装命令:

1
2
npm install hexo-generator-sitemap --save
npm install hexo-generator-baidu-sitemap --save

然后在站点配置文件_config.yml中找到如下对应的位置(一般默认有,没有的话可以添加),修改如下:

1
2
3
4
5
# 自动生成sitemap
sitemap:
path: sitemap.xml
baidusitemap:
path: baidusitemap.xml

特别要注意的是,上面的path一定要缩进,否则在hexo generate时会无法编译导致报错。(似乎有一些版本的hexo不存在这样的问题,关于版本可以使用hexo version命令查看)
这样以后每次generate后都会在public目录下面生成sitemap.xml和baidusitemap.xml两个文件,即你的站点地图。也可以deploy后直接在域名后面加上这两个文件名查看你的站点地图。
在百度站长平台中,有sitemap提交的选项,由于我当初提交的网站协议前缀是http,因此xml文件所在的https前缀的链接不属于我提交的网站,而我的github page和coding page都设置了强制https访问。这个问题以后有机会再做解决,不存在这个问题的可以试试提交sitemap。


疑惑

在优化的过程中,我发现我的post模板也被改变了(原因目前未知),从原本的:

1
2
3
4
5
title: {{ title }}
date: {{ date }}
tags:
copyright: true
top:

变成了:

1
2
3
4
5
6
7
8
9
10
11
---
noteId: "9bfafbb............dd5a3"
tags: []
title:
[object Object]: null
date:
[object Object]: null
copyright: true
top: null

---

更奇怪的是,我无法删除noteId并恢复到原来的样式,每次更改保存之后又会自动给我换回来,为了方便使用,我将其修改为:

1
2
3
4
5
6
7
8
9
10
11
---
noteId: "55c6d..............d6fcf"
title:
{{ title }}
date:
{{ date }}
copyright: true
top:
categories:
tags:
---

这样就可以直接提取文章标题和创建时间了。
对于noteId的作用,网上也找不到相关信息,可能是类似于网站的ID标识的一个代号吧,我对它之后的改进以及用法可见后文。


使用noteId改进url

今天看了几个url中含有noteId的网站,立马想到其实noteId其实可以用来替代url的中文等符号从而消除乱码,这更方便了爬虫的抓取。于是,我把站点配置文件下的url设置修改如下:

同时我把模板文件post.md修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
---
noteId: 'prefix+time remember to change!!!'
title:
{{ title }}
date:
{{ date }}
copyright: true
top:
categories:
tags:
---

<script type="text/javascript" src="/js/src/bai.js"></script>

这就把我的博文网址修改成了“关键词+创建时间”的形式,当然要手动更改。
同样的,以上的操作也带来了巨大的麻烦。我需要给之前没有生成noteId的博文一一加上noteId,同时也免不了对外部辅助平台和网站内链的大幅度修改。
对于检查网站死链,我推荐一个挺实用的轻量工具Xenu,下载安装之后,选择file,然后check URL,输入网站地址,即可检查站内所有的连接中是否存在死链。下面是我仅修改了url设置而未更改内链时检测的情况,其中红色的就是死链。


修改url之后

大约是在我修改了url格式的两天后,当我再用“site:url”查询收录情况时,我发现被收录的死链已经减少了一个(似乎不是提交死链的原因,因为规则提交的死链还在处理中),然而我之前被收录、修改后原url依然可用的主页和分类页面却也消失了,这就使我非常得纳闷。这几日也查找了许多资料寻找原因,总结如下。

首先,网站url的变动产生大量死链,很有可能会导致网站排名消失,原来积累的权重大大减少甚至清除。还好目前我只是一个新站,倘若已运行并被收录了一段时间,应该要慎重考虑是否是因为网址必须得精简等原因从而放弃网站的排名。要注意的是,如果网站url链接过深,会影响搜索引擎蜘蛛抓取,时间久了,蜘蛛来的次数就会减少,最后导致网站不收录。一般建议扁平化结构,url在三层以内方便蜘蛛爬行,这在上文也提到过。
此外,如果是新站的话,收录之后消失也是正常的。事实上,上线6个月之内的网站都可以被称为新站。因为搜索引擎蜘蛛对新站有一个好奇心,发现新鲜的事物喜欢去抓取一下,这就是收录,但是收录之后会有一个审核期,包括这个收录之后又消失的问题,审核期过后如果在数据库找不到相同的信息就会认为这是一篇原创,这个时候再去看收录就又会恢复了。值得注意的是,新站上线短期内,只新增更新内容就行了,不要去改动以前的内容(特别是标题、url等,搜索引擎对这些内容很敏感)以免延长新站考核时间,当网站索引趋于稳定状态后可以适当改动。
总而言之,目前没什么好担心的,担心也没有用,还是认认真真好好地写笔记好啦!


添加robots文件

Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站可以通过Robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。
如果一个网站使用大量的js、flash、ifrmae等内容,或者如果一个网站结构混乱,那么整个网站就会是乱七八糟、毫无章法,不仅用户体验极差,更重要的是蜘蛛也不会喜欢,也没有心思去抓取网站的内容了。
robots.txt是搜索引擎蜘蛛访问网站时要查看的第一个文件,并且会根据robots.txt文件的内容来爬行网站。在某种意义上说,它的一个任务就是指导蜘蛛爬行,减少搜索引擎蜘蛛的工作量。
当搜索引擎蜘蛛访问网站时,它会首先检查该站点根目录下是否存在robots.txt文件,如果该文件存在,搜索引擎蜘蛛就会按照该文件中的内容来确定爬行的范围;如果该文件不存在,则所有的搜索引擎蜘蛛将能够访问网站上所有没有被口令保护的页面。
通常搜索引擎对网站派出的蜘蛛是有配额的,多大规模的网站放出多少蜘蛛。如果我们不配置robots文件,那么蜘蛛来到网站以后会无目的地爬行,造成的一个结果就是,需要它爬行的目录,没有爬行到,不需要爬行的,也就是我们不想被收录的内容却被爬行并放出快照。所以robots文件对于SEO具有重要的意义。
如果网站中没有robots.txt文件,则网站中的程序脚本、样式表等一些和网站内容无关的文件或目录即使被搜索引擎蜘蛛爬行,也不会增加网站的收录率和权重,只会浪费服务器资源。此外,搜索引擎派出的蜘蛛资源也是有限的,我们要做的应该是尽量让蜘蛛爬行网站重点文件、目录,最大限度的节约蜘蛛资源。
在站点根目录的source文件下添加robots.txt文件,加入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
User-agent: * 
Allow: /
Allow: /archives/
Disallow: /categories/
Disallow: /tags/
Disallow: /vendors/
Disallow: /js/
Disallow: /css/
Disallow: /fonts/
Disallow: /vendors/
Disallow: /fancybox/

Sitemap: https://gsy00517.github.io/sitemap.xml
Sitemap: https://gsy00517.github.io/baidusitemap.xml

注意sitemap中要修改成自己的url。
另外,可以在站长工具平台检测robots文件。


碰到底线咯 后面没有啦

本文标题:hexo笔记:SEO优化

文章作者:高深远

发布时间:2019年11月01日 - 21:20

最后更新:2020年02月07日 - 17:19

原始链接:https://gsy00517.github.io/hexo20191101212014/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:文章排序 | 高深远的博客

hexo笔记:文章排序

说实话这个寒假真的是挺久的,希望疫情快点好转起来!
之前一直想着要让自己博客中的文章按照更新时间来排序,因为自己在学习的过程中总是会去更新之前的文章,特别是有时候更新量还挺大的,不放到前面来感觉有点可惜哈哈。但是由于没有时间而且不知道怎么做,一直没实现。今天突然想起这个事情,琢磨了好久终于搞出来了。
此次改动之后,我将博客侧栏中的“归档”修改为“最新发布”,依然是按照文章发布时间归档的。


主页文章按更新时间排序

实现前提:使用hexo搭建博客。
我先看了看hexo的配置文件中有没有可以直接设置的地方,试了几个关键词搜索之后发现果然有,这里原本是按照发布日期排序的。

1
2
3
4
5
6
7
8
# Home page setting
# path: Root path for your blogs index page. (default = '')
# per_page: Posts displayed per page. (0 = disable pagination)
# order_by: Posts order. (Order by date descending by default)
index_generator:
path: ''
per_page: 10
order_by: -date

看了一些hexo自动生成的代码之后,我发现了几个与date用法类似的参数,其中updateddate相对应。于是我将-date替换成了-updated,然后hexo start之后发现没起作用。
于是我参照着官方文档里的仅有的一点点说明,改成了-name试试能不能按名称排序,but failed。
没办法,我只好开始了漫长的对所有但凡是包含“date”的文件的阅览过程…网上也找不到任何资料,估计有这种想法的只有我吧哈哈。
终于,我在node_modules\hexo-generator-index-pin-top\lib\generator.js文件中找到了似乎是用于排序的代码,该文件是为了添加文章置顶功能的,但当置顶等级设置相同时,按照发布日期进行排序。
由于本人对JavaScript只是粗略了解,因此依样画葫芦作了一些修改,改完之后该文件中的内容如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
'use strict';
var pagination = require('hexo-pagination');
module.exports = function(locals){
var config = this.config;
var posts = locals.posts;
posts.data = posts.data.sort(function(a, b) {
if(a.top && b.top) { // 当两篇文章top都有定义时
if(a.top == b.top) return b.updated - a.updated; // 若top值一样,则按照文章更新日期降序排列
else return b.top - a.top; // 否则按照top值降序排列
}
else if(a.top && !b.top) { // 以下两种情况是若只有一篇文章top有定义,则将有top的排在前面(这里用异或操作居然不行233)
return -1;
}
else if(!a.top && b.top) { //上一条已解释
return 1;
}
else return b.updated - a.updated; // 若都没定义,则按照文章更新日期降序排列
});
var paginationDir = config.pagination_dir || 'page';
return pagination('', posts, {
perPage: config.index_generator.per_page,
layout: ['index', 'archive'],
format: paginationDir + '/%d/',
data: {
__index: true
}
});
};

再次hexo s,生效,开心。


侧栏按文章更新时间排序

实现前提:使用hexo搭建博客。
这是我在网上找到的另一种方法,由于和next主题不是特别搭配,因此最终没有使用。
首先在侧栏对应的目录下创建一个名为blog-sidebar.swig的渲染文件。(比如我的侧栏在header.swig中编辑,对应的目录为themes\next\layout\_partials,这里我使用了next主题,不同的话自行更改)
在该文件中添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="recent-posts">
<div class="item_headline" style="Text-align:center">
<span>最近更新<span>
</div>
<ul class="posts-list-block">
{% set posts = site.posts.sort('-updated') %}
{% for post in posts.slice('0', '10') %}
<li>
<a href="{{ url_for(post.path) }}" title="{{ post.title }}" target="_blank">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
</div>

然后来到编辑侧栏的文件,在合适的位置添加如下代码。

1
2
3
<div class="blog-sidebar-left">
{% include 'blog-sidebar.swig' %}
</div>

建议添加在nav标签内部,原因我在front-end笔记:使用div实现居中显示一文中曾作说明。


侧栏添加其它文章排序方式

实现前提:使用hexo搭建博客,使用next主题并使用leancloud做站点统计。
实现上面的功能之后,我又添加了“最近阅读”和“热度排名”两项。下面来说说我具体是怎么做的。

注:由于leancloud访问不是很稳定,在线浏览时或许会出错,但下面的方法确实是可行的。

按阅读量排行

在博客目录下执行hexo n page rank新建一个rank页面,编辑其中的index.md文件,添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="rank"></div>
<script src="https://cdn1.lncld.net/static/js/av-core-mini-0.6.4.js"></script>
<script>AV.initialize("YOUR_APPID", "YOUR_APPKEY");</script>
<script type="text/javascript">
var time=0
var title=""
var url=""
var query = new AV.Query('Counter');
query.notEqualTo('id',0);
query.descending('time');
query.limit(10);
query.find().then(function (todo) {
for (var i=0;i<10;i++){
var result=todo[i].attributes;
time=result.time;
title=result.title;
url=result.url;
var content="<p>"+"<font color='#1C1C1C'>"+time+"<i class='fa fa-eye' aria-hidden='true'></i>->"+"</font>"+"<a href='"+"YOUR_URL"+url+"'>"+title+"</a>"+"</p>";
document.getElementById("rank").innerHTML+=content
}
}, function (error) {
console.log("error");
});
</script>

注意,请将上面代码中的YOUR_APPIDYOUR_APPKEY替换为你learncloud中的对应ID和key,可在设置中查看。此外,还要将YOUR_URL替换为你的博客地址,包含协议并以/结束,例如我的:https://Gsy00517.github.io/

注意:请务必注意定义content变量时单引号和双引号的使用。

这时部署的话还是看不到这个page的,要看到排序,还需如下设置。
打开主题配置文件,在对应menu的位置添加rank: /rank/ || signal。这里的图标可以到Font Awesome上去找,非常丰富,我这里用的是signal。
如果你使用了语言包,别忘了到语言包内添加对应的翻译,比如我在hexo\theme\next\languages\zh-Hans.yml文件中对应menu的位置添加了rank: 热度排名

按最近阅读排行

在leancloud中,阅读量最近的更新时间用参数updatedAt表示,这是一个格式为YYYY-MM-DD HH:mm:ss的时间,不过我们也可以对它降序排列。
实现方法于上面的大同小异,这是我调整的代码,可在此基础上创造发挥,亲测有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="new"></div>
<script src="https://cdn1.lncld.net/static/js/av-core-mini-0.6.4.js"></script>
<script>AV.initialize("YOUR_APPID", "YOUR_APPKEY");</script>
<script type="text/javascript">
var updatedAt=""
var title=""
var url=""
var query = new AV.Query('Counter');
query.notEqualTo('id',0);
query.descending('updatedAt');
query.limit(10);
query.find().then(function (todo) {
for (var i=0;i<10;i++){
var result=todo[i].attributes;
title=result.title;
updatedAt=result.updatedAt;
url=result.url;
var content="<p>"+"<a href='"+"YOUR_URL"+url+"' target='_blank'>"+title+"</a>"+"</p>";
document.getElementById("new").innerHTML+=content
}
}, function (error) {
console.log("error");
});
</script>


碰到底线咯 后面没有啦

本文标题:hexo笔记:文章排序

文章作者:高深远

发布时间:2020年02月07日 - 15:13

最后更新:2020年02月09日 - 16:13

原始链接:https://gsy00517.github.io/hexo20200207151318/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
hexo笔记:文章排序 | 高深远的博客

hexo笔记:文章排序

说实话这个寒假真的是挺久的,希望疫情快点好转起来!
之前一直想着要让自己博客中的文章按照更新时间来排序,因为自己在学习的过程中总是会去更新之前的文章,特别是有时候更新量还挺大的,不放到前面来感觉有点可惜哈哈。但是由于没有时间而且不知道怎么做,一直没实现。今天突然想起这个事情,琢磨了好久终于搞出来了。
此次改动之后,我将博客侧栏中的“归档”修改为“最新发布”,依然是按照文章发布时间归档的。


主页文章按更新时间排序

实现前提:使用hexo搭建博客。
我先看了看hexo的配置文件中有没有可以直接设置的地方,试了几个关键词搜索之后发现果然有,这里原本是按照发布日期排序的。

1
2
3
4
5
6
7
8
# Home page setting
# path: Root path for your blogs index page. (default = '')
# per_page: Posts displayed per page. (0 = disable pagination)
# order_by: Posts order. (Order by date descending by default)
index_generator:
path: ''
per_page: 10
order_by: -date

看了一些hexo自动生成的代码之后,我发现了几个与date用法类似的参数,其中updateddate相对应。于是我将-date替换成了-updated,然后hexo start之后发现没起作用。
于是我参照着官方文档里的仅有的一点点说明,改成了-name试试能不能按名称排序,but failed。
没办法,我只好开始了漫长的对所有但凡是包含“date”的文件的阅览过程…网上也找不到任何资料,估计有这种想法的只有我吧哈哈。
终于,我在node_modules\hexo-generator-index-pin-top\lib\generator.js文件中找到了似乎是用于排序的代码,该文件是为了添加文章置顶功能的,但当置顶等级设置相同时,按照发布日期进行排序。
由于本人对JavaScript只是粗略了解,因此依样画葫芦作了一些修改,改完之后该文件中的内容如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
'use strict';
var pagination = require('hexo-pagination');
module.exports = function(locals){
var config = this.config;
var posts = locals.posts;
posts.data = posts.data.sort(function(a, b) {
if(a.top && b.top) { // 当两篇文章top都有定义时
if(a.top == b.top) return b.updated - a.updated; // 若top值一样,则按照文章更新日期降序排列
else return b.top - a.top; // 否则按照top值降序排列
}
else if(a.top && !b.top) { // 以下两种情况是若只有一篇文章top有定义,则将有top的排在前面(这里用异或操作居然不行233)
return -1;
}
else if(!a.top && b.top) { //上一条已解释
return 1;
}
else return b.updated - a.updated; // 若都没定义,则按照文章更新日期降序排列
});
var paginationDir = config.pagination_dir || 'page';
return pagination('', posts, {
perPage: config.index_generator.per_page,
layout: ['index', 'archive'],
format: paginationDir + '/%d/',
data: {
__index: true
}
});
};

再次hexo s,生效,开心。


侧栏按文章更新时间排序

实现前提:使用hexo搭建博客。
这是我在网上找到的另一种方法,由于和next主题不是特别搭配,因此最终没有使用。
首先在侧栏对应的目录下创建一个名为blog-sidebar.swig的渲染文件。(比如我的侧栏在header.swig中编辑,对应的目录为themes\next\layout\_partials,这里我使用了next主题,不同的话自行更改)
在该文件中添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="recent-posts">
<div class="item_headline" style="Text-align:center">
<span>最近更新<span>
</div>
<ul class="posts-list-block">
{% set posts = site.posts.sort('-updated') %}
{% for post in posts.slice('0', '10') %}
<li>
<a href="{{ url_for(post.path) }}" title="{{ post.title }}" target="_blank">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
</div>

然后来到编辑侧栏的文件,在合适的位置添加如下代码。

1
2
3
<div class="blog-sidebar-left">
{% include 'blog-sidebar.swig' %}
</div>

建议添加在nav标签内部,原因我在front-end笔记:使用div实现居中显示一文中曾作说明。


侧栏添加其它文章排序方式

实现前提:使用hexo搭建博客,使用next主题并使用leancloud做站点统计。
实现上面的功能之后,我又添加了“最近阅读”和“热度排名”两项。下面来说说我具体是怎么做的。

注:由于leancloud访问不是很稳定,在线浏览时或许会出错,但下面的方法确实是可行的。

按阅读量排行

在博客目录下执行hexo n page rank新建一个rank页面,编辑其中的index.md文件,添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="rank"></div>
<script src="https://cdn1.lncld.net/static/js/av-core-mini-0.6.4.js"></script>
<script>AV.initialize("YOUR_APPID", "YOUR_APPKEY");</script>
<script type="text/javascript">
var time=0
var title=""
var url=""
var query = new AV.Query('Counter');
query.notEqualTo('id',0);
query.descending('time');
query.limit(10);
query.find().then(function (todo) {
for (var i=0;i<10;i++){
var result=todo[i].attributes;
time=result.time;
title=result.title;
url=result.url;
var content="<p>"+"<font color='#1C1C1C'>"+time+"<i class='fa fa-eye' aria-hidden='true'></i>->"+"</font>"+"<a href='"+"YOUR_URL"+url+"'>"+title+"</a>"+"</p>";
document.getElementById("rank").innerHTML+=content
}
}, function (error) {
console.log("error");
});
</script>

注意,请将上面代码中的YOUR_APPIDYOUR_APPKEY替换为你learncloud中的对应ID和key,可在设置中查看。此外,还要将YOUR_URL替换为你的博客地址,包含协议并以/结束,例如我的:https://Gsy00517.github.io/

注意:请务必注意定义content变量时单引号和双引号的使用。

这时部署的话还是看不到这个page的,要看到排序,还需如下设置。
打开主题配置文件,在对应menu的位置添加rank: /rank/ || signal。这里的图标可以到Font Awesome上去找,非常丰富,我这里用的是signal。
如果你使用了语言包,别忘了到语言包内添加对应的翻译,比如我在hexo\theme\next\languages\zh-Hans.yml文件中对应menu的位置添加了rank: 热度排名

按最近阅读排行

在leancloud中,阅读量最近的更新时间用参数updatedAt表示,这是一个格式为YYYY-MM-DD HH:mm:ss的时间,不过我们也可以对它降序排列。
实现方法于上面的大同小异,这是我调整的代码,可在此基础上创造发挥,亲测有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="new"></div>
<script src="https://cdn1.lncld.net/static/js/av-core-mini-0.6.4.js"></script>
<script>AV.initialize("YOUR_APPID", "YOUR_APPKEY");</script>
<script type="text/javascript">
var updatedAt=""
var title=""
var url=""
var query = new AV.Query('Counter');
query.notEqualTo('id',0);
query.descending('updatedAt');
query.limit(10);
query.find().then(function (todo) {
for (var i=0;i<10;i++){
var result=todo[i].attributes;
title=result.title;
updatedAt=result.updatedAt;
url=result.url;
var content="<p>"+"<a href='"+"YOUR_URL"+url+"' target='_blank'>"+title+"</a>"+"</p>";
document.getElementById("new").innerHTML+=content
}
}, function (error) {
console.log("error");
});
</script>


碰到底线咯 后面没有啦

本文标题:hexo笔记:文章排序

文章作者:高深远

发布时间:2020年02月07日 - 15:13

最后更新:2020年02月09日 - 16:13

原始链接:https://gsy00517.github.io/hexo20200207151318/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
高深远的博客高深远的博客jupyter notebook笔记:一些细小的操作 | 高深远的博客

jupyter notebook笔记:一些细小的操作

首先强烈安利jupyter notebook,它是一种交互式笔记本,安装anaconda的时候会一并安装,下载VS的时候也可以选择安装。
我是在初学机器学习的时候接触jupyter notebook的,立刻就被它便捷的交互与结果呈现方式所吸引,现在python编程基本不使用其他的软件。其实完全可以在初学python的时候使用jupyter notebook,可以立即得到反馈以及分析错误,可以进步很快!


打开操作

当初安装好之后还不了解,每次打开jupyter notebook都会先弹出一个黑框框,这时候千万别关掉,等一会就能来到网页。另外打开之后也别关掉,使用的时候是一直需要的,因为只有开着才能访问本机web服务器发布的内容。

另外你也可以不通过快捷方式,直接在命令行中直接输入jupyter notebook来打开它。
有些时候,当你插入硬盘或者需要直接在特定的目录下打开jupyter notebook(它的默认打开是在“usr/用户名/”路径下),那你可以在输入命令的后面加上你想要的路径。

1
2
3
jupyter notebook D:\ #打开D盘,于我是我的移动硬盘
jupyter notebook E:\ #我的U盘
jupyter notebook C:\Users\用户名\Desktop #在桌面打开


快捷键操作

在jupyter notebook中可以通过选中cell然后按h的方式查询快捷键。


其他

在jupyter notebook中可以直接使用markdown,这对学习可以起到很大的辅助作用,markdown的基本操作可以看我的另一篇博文markdown笔记:markdown的基本使用
此外,在我的博文anaconda笔记:conda的各种命令行操作中,也介绍了如何将python的虚拟环境添加到jupyter notebook中,欢迎阅读。
VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试,不过我在kernel配置方面似乎还有一些小问题有待解决。


碰到底线咯 后面没有啦

本文标题:jupyter notebook笔记:一些细小的操作

文章作者:高深远

发布时间:2019年09月13日 - 22:50

最后更新:2019年11月10日 - 17:11

原始链接:https://gsy00517.github.io/jupyter-notebook20190913225022/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
jupyter notebook笔记:一些细小的操作 | 高深远的博客

jupyter notebook笔记:一些细小的操作

首先强烈安利jupyter notebook,它是一种交互式笔记本,安装anaconda的时候会一并安装,下载VS的时候也可以选择安装。
我是在初学机器学习的时候接触jupyter notebook的,立刻就被它便捷的交互与结果呈现方式所吸引,现在python编程基本不使用其他的软件。其实完全可以在初学python的时候使用jupyter notebook,可以立即得到反馈以及分析错误,可以进步很快!


打开操作

当初安装好之后还不了解,每次打开jupyter notebook都会先弹出一个黑框框,这时候千万别关掉,等一会就能来到网页。另外打开之后也别关掉,使用的时候是一直需要的,因为只有开着才能访问本机web服务器发布的内容。

另外你也可以不通过快捷方式,直接在命令行中直接输入jupyter notebook来打开它。
有些时候,当你插入硬盘或者需要直接在特定的目录下打开jupyter notebook(它的默认打开是在“usr/用户名/”路径下),那你可以在输入命令的后面加上你想要的路径。

1
2
3
jupyter notebook D:\ #打开D盘,于我是我的移动硬盘
jupyter notebook E:\ #我的U盘
jupyter notebook C:\Users\用户名\Desktop #在桌面打开


快捷键操作

在jupyter notebook中可以通过选中cell然后按h的方式查询快捷键。


其他

在jupyter notebook中可以直接使用markdown,这对学习可以起到很大的辅助作用,markdown的基本操作可以看我的另一篇博文markdown笔记:markdown的基本使用
此外,在我的博文anaconda笔记:conda的各种命令行操作中,也介绍了如何将python的虚拟环境添加到jupyter notebook中,欢迎阅读。
VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试,不过我在kernel配置方面似乎还有一些小问题有待解决。


碰到底线咯 后面没有啦

本文标题:jupyter notebook笔记:一些细小的操作

文章作者:高深远

发布时间:2019年09月13日 - 22:50

最后更新:2019年11月10日 - 17:11

原始链接:https://gsy00517.github.io/jupyter-notebook20190913225022/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集 | 高深远的博客

kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集

kaggle是一个著名的数据科学竞赛平台,暑假里我也抽空自己独立完成了三四个getting started级别的比赛。对于MNIST数据集,想必入门计算机视觉的人应该也不会陌生。kaggle上getting started的第一个比赛就是Digit Recognizer:Learn computer vision fundamentals with the famous MNIST data。当时作为入门小白的我,使用了入门级的方法KNN完成了我的第一次机器学习(自认为KNN是最最基础的算法,对它的介绍可见我的另一篇博文machine-learning笔记:机器学习的几个常见算法及其优缺点,真的非常简单,但也极其笨拙)。而最近我又使用CNN再一次尝试了这个数据集,踩了不少坑,因此想把两次经历统统记录在这,可能会有一些不足之处,留作以后整理优化。

References

电子文献:
https://blog.csdn.net/gybinlero/article/details/79294649
https://blog.csdn.net/qq_43497702/article/details/95005248
https://blog.csdn.net/a19990412/article/details/90349429


KNN

首先导入必要的包,这里基本用不到太多:

1
2
3
4
5
6
7
import numpy as np
import csv
import operator

import matplotlib
from matplotlib import pyplot as plt
%matplotlib inline

导入训练数据:

1
2
3
4
5
6
7
8
9
10
trainSet = []
with open('train.csv','r') as trainFile:
lines=csv.reader(trainFile)
for line in lines:
trainSet.append(line)
trainSet.remove(trainSet[0])

trainSet = np.array(trainSet)
rawTrainLabel = trainSet[:, 0] #分割出训练集标签
rawTrainData = trainSet[:, 1:] #分割出训练集数据

我当时用了一种比较笨拙的办法转换数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
rawTrainData = np.mat(rawTrainData) #转化成矩阵,或许不需要
m, n = np.shape(rawTrainData)
trainData = np.zeros((m, n)) #创建初值为0的ndarray
for i in range(m):
for j in range(n):
trainData[i, j] = int(rawTrainData[i, j]) #转化并赋值

rawTrainLabel = np.mat(rawTrainLabel) #或许不需要
m, n = np.shape(rawTrainLabel)
trainLabel = np.zeros((m, n))
for i in range(m):
for j in range(n):
trainLabel[i, j] = int(rawTrainLabel[i, j])

这里我们可以查看以下数据的维度,确保没有出错。

为了方便起见,我们把所有pixel不为0的点都设置为1。

1
2
3
4
5
m, n = np.shape(trainData)
for i in range(m):
for j in range(n):
if trainData[i, j] != 0:
trainData[i, j] = 1

仿照训练集的步骤,导入测试集并做相同处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
testSet = []
with open('test.csv','r') as testFile:
lines=csv.reader(testFile)
for line in lines:
testSet.append(line)
testSet.remove(testSet[0])

testSet = np.array(testSet)
rawTestData = testSet

rawTestData = np.mat(rawTestData)
m, n = np.shape(rawTestData)
testData = np.zeros((m, n))
for i in range(m):
for j in range(n):
testData[i, j] = int(rawTestData[i, j])

m, n = np.shape(testData)
for i in range(m):
for j in range(n):
if testData[i, j] != 0:
testData[i, j] = 1

同样的,可使用testData.shape查看测试集的维度,保证它是28000*784,由此可知操作无误。
接下来,我们定义KNN的分类函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def classify(inX, dataSet, labels, k):
inX = np.mat(inX)
dataSet = np.mat(dataSet)
labels = np.mat(labels)
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = np.array(diffMat) ** 2
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances ** 0.5
sortedDistIndicies = distances.argsort()
classCount={}
for i in range(k):
voteIlabel = labels[0, sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse = True)
return sortedClassCount[0][0]

为了更好地分类,这里我们需要选择合适的k值,我选取了4000个样本作为验证机进行尝试,找到误差最小的k值并作为最终的k值输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
trainingTestSize = 4000

#分割出验证集
m, n = np.shape(trainLabel)
trainingTrainLabel = np.zeros((m, n - trainingTestSize))
for i in range(m):
for j in range(n - trainingTestSize):
trainingTrainLabel[i, j] = trainLabel[i, j]

trainingTestLabel = np.zeros((m, trainingTestSize))
for i in range(m):
for j in range(trainingTestSize):
trainingTestLabel[i, j] = trainLabel[i, n - trainingTestSize + j]

m, n = np.shape(trainData)
trainingTrainData = np.zeros((m - trainingTestSize, n))
for i in range(m - trainingTestSize):
for j in range(n):
trainingTrainData[i, j] = trainData[i, j]

trainingTestData = np.zeros((trainingTestSize, n))
for i in range(trainingTestSize):
for j in range(n):
trainingTestData[i, j] = trainData[m - trainingTestSize + i, j]

#使k值为3到9依次尝试
training = []
for x in range(3, 10):
error = 0
for y in range(trainingTestSize):
answer = (classify(trainingTestData[y], trainingTrainData, trainingTrainLabel, x))
print 'the classifier came back with: %d, %.2f%% has done, the k now is %d' % (answer, (y + (x - 3) * trainingTestSize) / float(trainingTestSize * 7) * 100, x) #方便知道进度
if answer != trainingTestLabel[0, y]:
error += 1
training.append(error)

这个过程比较长,结果会得到training的结果是[156, 173, 159, 164, 152, 155, 156]。
可以使用plt.plot(training)更直观地查看误差,呈现如下:

注意:这里的下标应该加上3才是对应的k值。

可以看图手动选择k值,但由于事先无法把握训练结束的时间,可以编写函数自动选择并使程序继续进行。

1
2
3
4
5
6
theK = 3
hasError = training[0]
for i in range(7):
if training[i] < hasError:
theK = i + 3
hasError = training[i]

在确定k值后,接下来就是代入测试集进行结果的计算了。由于KNN算法相对而言比较低级,因此就别指望效率了,跑CPU的话整个过程大概需要半天左右。

1
2
3
4
5
6
m, n = np.shape(testData)
result = []
for i in range(m):
answer = (classify(testData[i], trainData, trainLabel, theK))
result.append(answer)
print 'the classifier came back with: %d, %.2f%% has done' % (answer, i / float(m) * 100)

最后,定义一个保存结果的函数,然后saveResult(result)之后,再对csv文件进行处理(后文会提到),然后就可以submit了。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最终此方法在kaggle上获得的score为0.96314,准确率还是挺高的,主要是因为问题相对简单,放到leaderboard上,这结果的排名就要到两千左右了。


CNN

在学习了卷积神经网络和pytorch框架之后,我决定使用CNN对这个比赛再进行一次尝试。
首先还是导入相关的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import pandas as pd
import numpy as np
from math import *

%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.cm as cm

import torch.utils.data as Data

from torch.autograd import Variable

import csv

导入训练数据,可以使用train.head()查看导入的结果,便于后续的处理。

1
train = pd.read_csv("train.csv")

对数据进行处理,由于要使用的是CNN,我们必须要把数据整理成能输入的形式,即从数组变成高维张量。

1
2
3
4
5
train_labels = torch.from_numpy(np.array(train.label[:]))

image_size = train.iloc[:, 1:].shape[1]
image_width = image_height = np.ceil(np.sqrt(image_size)).astype(np.uint8)
train_data = torch.FloatTensor(np.array(train.iloc[:, 1:]).reshape((-1, 1, image_width, image_height))) / 255 #灰度压缩,进行归一化

注:reshape中的-1表示自适应,这样我们能让我们更好的变化数据的形式。

我们可以使用matplotlib查看数据处理的结果。

1
2
3
plt.imshow(train_data[1].numpy().squeeze(), cmap = 'gray')
plt.title('%i' % train_labels[1])
plt.show()

可以看到如下图片,可以与plt.title进行核对。

注:可以用squeeze()函数来降维,例如:从[[1]]—>[1]
与之相反的是便是unsqueeze(dim = 1),该函数可以使[1]—>[[1]]

以同样的方式导入并处理测试集。

1
2
test= pd.read_csv("test.csv")
test_data = torch.FloatTensor(np.array(test).reshape((-1, 1, image_width, image_height))) / 255

接下来我们定义几个超参数,这里将要使用的是小批梯度下降的优化算法,因此定义如下:

1
2
3
4
#超参数
EPOCH = 1 #整个数据集循环训练的轮数
BATCH_SIZE = 10 #每批的样本个数
LR = 0.01 #学习率

定义好超参数之后,我们使用Data对数据进行最后的处理。

1
2
3
4
5
6
7
trainData = Data.TensorDataset(train_data, train_labels) #用后会变成元组类型

train_loader = Data.DataLoader(
dataset = trainData,
batch_size = BATCH_SIZE,
shuffle = True
)

上面的Data.TensorDataset可以把数据进行打包,以方便我们更好的使用;而Data.DataLoade可以将我们的数据打乱并且分批。要注意的是,这里不要对测试集进行操作,否则最终输出的结果就难以再与原来的顺序匹配了。
接下来,我们定义卷积神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#build CNN
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
#一个卷积层
self.conv1 = nn.Sequential(
nn.Conv2d( #输入(1, 28, 28)
in_channels = 1, #1个通道
out_channels = 16, #输出层数
kernel_size = 5, #过滤器的大小
stride = 1, #步长
padding = 2 #填白
), #输出(16, 28, 28)
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
)
self.conv2 = nn.Sequential( #输入(16, 14, 14)
nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
)
self.out = nn.Linear(32 * 7 * 7, 10) #全连接层,将三维的数据展为2维的数据并输出

def forward(self, x): #父类已定义,不能修改名字
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
output = F.softmax(self.out(x))
return output

cnn = CNN()
optimzer = torch.optim.Adam(cnn.parameters(), lr = LR) #define optimezer
loss_func = nn.CrossEntropyLoss() #loss function使用交叉嫡误差

print(cnn) # 查看net architecture

完成以上的操作之后,就可以开始训练了,整个训练时间在CPU上只需要几分钟,这比KNN算法要优越许多。

1
2
3
4
5
6
7
8
9
10
11
12
for epoch in range(EPOCH):
for step, (x, y) in enumerate(train_loader):
b_x = Variable(x)
b_y = Variable(y)
output = cnn(b_x)
loss = loss_func(output, b_y) #cross entropy loss
#update W
optimzer.zero_grad()
loss.backward()
optimzer.step()
print('epoch%d' % (epoch + 1), '-', 'batch%d' % step, '-', 'loss%f' % loss) #查看训练过程
print('No.%depoch is over' % (epoch + 1))

代入测试集求解:

1
2
3
4
5
output = cnn(test_data[:])
#print(output)

result = torch.max(output, 1)[1].squeeze()
#print(result)

仿照KNN中的结果转存函数,定义saveResult函数。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最后使用saveResult(result.numpy())把结果存入csv文件。


改进

然而,若使用上述的CNN,得出的结果在leaderboard上会达到两千三百多名,这已经进入所有参赛者的倒数两百名之内了。为什么这个CNN的表现甚至不如我前面的KNN算法呢?我觉得主要有下面三个原因。

  1. 首先,由于CNN的参数较多,仅经过1轮epoch应该是不足够把所有参数训练到最优或者接近最优的位置的。个人认为,靠前的数据在参数相对远离最优值时参与训练而在之后不起作用,很有可能导致最后顾此失彼,因此有必要增加epoch使之前的数据多次参与参数的校正。同时,也要增大batch size使每次优化参数使用的样本更多,从而在测试集上表现更好。训练结束后,我发现我的C盘会被占用几个G,不知道是不是出错了,也有可能是参数占用的空间,必须停止kernel才能得到释放(我关闭了VScode后刷新,空间就回来了)。关于内存,这里似乎存在着一个问题,我将在后文阐述。

    注:由于VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试。

  2. 学习率过大。尽管我这里的学习率设置为0.01,但对于最后的收敛来说或许还是偏大,这就导致了最后会在最优解附近来回抖动而难以接近的问题。关于这个问题,可以到deep-learning笔记:学习率衰减与批归一化中看看我较为详细的分析与解决方法。

  3. 由于训练时间和epoch轮数相对较小,我推测模型可能会存在过拟合的问题。尤其是最后的全连接层,它的结构很容易造成过拟合。关于这个问题,也可以到machine-learning笔记:过拟合与欠拟合machine-learning笔记:机器学习中正则化的理解中看看我较为详细的分析与解决方法。

针对上述原因,我对我的CNN模型做了如下调整:

  1. 首先,增加训练量,调整超参数如下。

    1
    2
    3
    4
    #超参数
    EOPCH = 3
    BATCH_SIZE = 50
    LR = 1e-4
  2. 引入dropout随机失活,加强全连接层的鲁棒性,修改网络结构如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #build CNN
    class CNN(nn.Module):
    def __init__(self):
    super(CNN, self).__init__()
    #一个卷积层
    self.conv1 = nn.Sequential(
    nn.Conv2d( #输入(1, 28, 28)
    in_channels = 1, #1个通道
    out_channels = 16, #输出层数
    kernel_size = 5, #过滤器的大小
    stride = 1, #步长
    padding = 2 #填白
    ), #输出(16, 28, 28)
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
    )
    self.conv2 = nn.Sequential( #输入(16, 14, 14)
    nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
    )

    self.dropout = nn.Dropout(p = 0.5) #每次减少50%神经元之间的连接

    self.fc = nn.Linear(32 * 7 * 7, 1024)
    self.out = nn.Linear(1024, 10) #全连接层,将三维的数据展为2维的数据并输出

    def forward(self, x):
    x = self.conv1(x)
    x = self.conv2(x)
    x = x.view(x.size(0), -1)
    x = self.fc(x)
    x = self.dropout(x)
    output = F.softmax(self.out(x))
    return output

本想直接使用torch.nn.functional中的dropout函数轻松实现随机失活正则化,但在网上看到这个函数好像有点坑,因此就不以身试坑了,还是在网络初始化中先定义dropout。

注:训练完新定义的网络之后我一直在思考dropout添加的方式与位置。在看了一些资料之后,我认为或许去掉全连接层、保持原来的层数并在softmax之前dropout可能能达到更好的效果。考虑到知乎上有知友提到做研究试验不宜在MNIST这些玩具级别的数据集上进行,因此暂时不再做没有太大意义的调整,今后有空在做改进试验。

经过上面的改进后,我再次训练网络并提交结果,在kaggle上的评分提高至0.97328,大约处在1600名左右,可以继续调整超参数(可以分割验证集寻找)和加深网络结构以取得更高的分数,但我的目的已经达到了。与之前的KNN相比,无论从时间效率还是准确率,CNN都有很大的进步,这也体现了深度学习相对于一些经典机器学习算法的优势。


出现的问题

由于这个最后的网络是我重复构建之后完成的,因此下列部分问题可能不存在于我上面的代码中,但我还是想汇总在这,以防之后继续踩相同的坑。

  1. 报错element 0 of tensors does not require grad and does not have a grad_fn

    pytorch具有自动求导机制,这就省去了我们编写反向传播的代码。每个Variable变量都有两个标志:requires_grad和volatile。出现上述问题的原因是requires_grad = False,修改或者增加(因为默认是false)成True即可。
  2. RuntimeError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

    这个好像是我在计算交叉熵时遇到的,原因是因为torch的交叉熵的输入第一个位置的输入应该是在每个label下的概率,而不是对应的label,详细分析与举例可参考文首给出的第三个链接。
  3. AttributeError: ‘tuple’ object has no attribute ‘numpy’

    为了查看数据处理效果,我在数据预处理过程中使用matplotlib绘制出处理后的图像,但是却出现了如上报错,当时的代码如下:

    1
    2
    3
    plt.imshow(trainData[1].numpy().squeeze(), cmap = 'gray')
    plt.title('%i' % train_labels[1])
    plt.show()

    查找相关资料之后,我才知道torch.utils.data会把打包的数据变成元组类型,因此我们绘图还是要使用原来train_data中的数据。

  4. 转存结果时提醒DefaultCPUAllocator: not enough memory

    由于当初在实现KNN算法转存结果时使用的函数存入csv文件后还要对文件进行空值删除处理,比较麻烦(后文会写具体如何处理),因此我想借用文章顶部给出的第二个链接中提供的方法:

    1
    2
    3
    out = pd.DataFrame(np.array(result), index = range(1, 1 + len(result)), columns = ['ImageId', 'Label'])
    #torch和pandas的类型不能直接的转换,所以需要借助numpy中间的步骤,将torch的数据转给pandas
    out.to_csv('result.csv', header = None)

    结果出现如下错误:

    我好歹也是八千多买的DELL旗舰本,8G内存,它居然说我不够让我换块新的RAM?什么情况…
    尝试许久,我怀疑是训练得到的参数占用了我的内存,那只好先把训练出的result保存下来,再导入到csv文件。
    最后我还是选择自己手动处理csv文件中的空值,应该有其它的转存csv文件的方法或者上述问题的解决措施,留待以后实践过程中发现解决,也欢迎大家不吝赐教。


excel/csv快速删除空白行

如果你使用的是我的saveResult函数或者类似,你就很有可能发现更新后的csv文件中数据之间双数行都是留空的,即一列数据之间都有空白行相隔,那么可以使用如下方法快速删除空白行。

  1. 选中对应列或者区域。
  2. 在“开始”工具栏中找到“查找与选择”功能并点击。
  3. 在下拉菜单中,点击“定位条件”选项。
  4. 在打开的定位条件窗口中,选择“空值”并确定。
  5. 待电脑为你选中所有空值后,任意右键一个被选中的空白行,在弹出的菜单中点击“删除”。
  6. 如果数据量比较大,这时候会有一个处理时间可能会比较长的提醒弹出,确认即可。
  7. 等待处理完毕。

碰到底线咯 后面没有啦

本文标题:kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集

文章作者:高深远

发布时间:2019年11月02日 - 11:24

最后更新:2020年02月07日 - 17:22

原始链接:https://gsy00517.github.io/kaggle20191102112435/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集 | 高深远的博客

kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集

kaggle是一个著名的数据科学竞赛平台,暑假里我也抽空自己独立完成了三四个getting started级别的比赛。对于MNIST数据集,想必入门计算机视觉的人应该也不会陌生。kaggle上getting started的第一个比赛就是Digit Recognizer:Learn computer vision fundamentals with the famous MNIST data。当时作为入门小白的我,使用了入门级的方法KNN完成了我的第一次机器学习(自认为KNN是最最基础的算法,对它的介绍可见我的另一篇博文machine-learning笔记:机器学习的几个常见算法及其优缺点,真的非常简单,但也极其笨拙)。而最近我又使用CNN再一次尝试了这个数据集,踩了不少坑,因此想把两次经历统统记录在这,可能会有一些不足之处,留作以后整理优化。

References

电子文献:
https://blog.csdn.net/gybinlero/article/details/79294649
https://blog.csdn.net/qq_43497702/article/details/95005248
https://blog.csdn.net/a19990412/article/details/90349429


KNN

首先导入必要的包,这里基本用不到太多:

1
2
3
4
5
6
7
import numpy as np
import csv
import operator

import matplotlib
from matplotlib import pyplot as plt
%matplotlib inline

导入训练数据:

1
2
3
4
5
6
7
8
9
10
trainSet = []
with open('train.csv','r') as trainFile:
lines=csv.reader(trainFile)
for line in lines:
trainSet.append(line)
trainSet.remove(trainSet[0])

trainSet = np.array(trainSet)
rawTrainLabel = trainSet[:, 0] #分割出训练集标签
rawTrainData = trainSet[:, 1:] #分割出训练集数据

我当时用了一种比较笨拙的办法转换数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
rawTrainData = np.mat(rawTrainData) #转化成矩阵,或许不需要
m, n = np.shape(rawTrainData)
trainData = np.zeros((m, n)) #创建初值为0的ndarray
for i in range(m):
for j in range(n):
trainData[i, j] = int(rawTrainData[i, j]) #转化并赋值

rawTrainLabel = np.mat(rawTrainLabel) #或许不需要
m, n = np.shape(rawTrainLabel)
trainLabel = np.zeros((m, n))
for i in range(m):
for j in range(n):
trainLabel[i, j] = int(rawTrainLabel[i, j])

这里我们可以查看以下数据的维度,确保没有出错。

为了方便起见,我们把所有pixel不为0的点都设置为1。

1
2
3
4
5
m, n = np.shape(trainData)
for i in range(m):
for j in range(n):
if trainData[i, j] != 0:
trainData[i, j] = 1

仿照训练集的步骤,导入测试集并做相同处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
testSet = []
with open('test.csv','r') as testFile:
lines=csv.reader(testFile)
for line in lines:
testSet.append(line)
testSet.remove(testSet[0])

testSet = np.array(testSet)
rawTestData = testSet

rawTestData = np.mat(rawTestData)
m, n = np.shape(rawTestData)
testData = np.zeros((m, n))
for i in range(m):
for j in range(n):
testData[i, j] = int(rawTestData[i, j])

m, n = np.shape(testData)
for i in range(m):
for j in range(n):
if testData[i, j] != 0:
testData[i, j] = 1

同样的,可使用testData.shape查看测试集的维度,保证它是28000*784,由此可知操作无误。
接下来,我们定义KNN的分类函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def classify(inX, dataSet, labels, k):
inX = np.mat(inX)
dataSet = np.mat(dataSet)
labels = np.mat(labels)
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = np.array(diffMat) ** 2
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances ** 0.5
sortedDistIndicies = distances.argsort()
classCount={}
for i in range(k):
voteIlabel = labels[0, sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse = True)
return sortedClassCount[0][0]

为了更好地分类,这里我们需要选择合适的k值,我选取了4000个样本作为验证机进行尝试,找到误差最小的k值并作为最终的k值输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
trainingTestSize = 4000

#分割出验证集
m, n = np.shape(trainLabel)
trainingTrainLabel = np.zeros((m, n - trainingTestSize))
for i in range(m):
for j in range(n - trainingTestSize):
trainingTrainLabel[i, j] = trainLabel[i, j]

trainingTestLabel = np.zeros((m, trainingTestSize))
for i in range(m):
for j in range(trainingTestSize):
trainingTestLabel[i, j] = trainLabel[i, n - trainingTestSize + j]

m, n = np.shape(trainData)
trainingTrainData = np.zeros((m - trainingTestSize, n))
for i in range(m - trainingTestSize):
for j in range(n):
trainingTrainData[i, j] = trainData[i, j]

trainingTestData = np.zeros((trainingTestSize, n))
for i in range(trainingTestSize):
for j in range(n):
trainingTestData[i, j] = trainData[m - trainingTestSize + i, j]

#使k值为3到9依次尝试
training = []
for x in range(3, 10):
error = 0
for y in range(trainingTestSize):
answer = (classify(trainingTestData[y], trainingTrainData, trainingTrainLabel, x))
print 'the classifier came back with: %d, %.2f%% has done, the k now is %d' % (answer, (y + (x - 3) * trainingTestSize) / float(trainingTestSize * 7) * 100, x) #方便知道进度
if answer != trainingTestLabel[0, y]:
error += 1
training.append(error)

这个过程比较长,结果会得到training的结果是[156, 173, 159, 164, 152, 155, 156]。
可以使用plt.plot(training)更直观地查看误差,呈现如下:

注意:这里的下标应该加上3才是对应的k值。

可以看图手动选择k值,但由于事先无法把握训练结束的时间,可以编写函数自动选择并使程序继续进行。

1
2
3
4
5
6
theK = 3
hasError = training[0]
for i in range(7):
if training[i] < hasError:
theK = i + 3
hasError = training[i]

在确定k值后,接下来就是代入测试集进行结果的计算了。由于KNN算法相对而言比较低级,因此就别指望效率了,跑CPU的话整个过程大概需要半天左右。

1
2
3
4
5
6
m, n = np.shape(testData)
result = []
for i in range(m):
answer = (classify(testData[i], trainData, trainLabel, theK))
result.append(answer)
print 'the classifier came back with: %d, %.2f%% has done' % (answer, i / float(m) * 100)

最后,定义一个保存结果的函数,然后saveResult(result)之后,再对csv文件进行处理(后文会提到),然后就可以submit了。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最终此方法在kaggle上获得的score为0.96314,准确率还是挺高的,主要是因为问题相对简单,放到leaderboard上,这结果的排名就要到两千左右了。


CNN

在学习了卷积神经网络和pytorch框架之后,我决定使用CNN对这个比赛再进行一次尝试。
首先还是导入相关的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import pandas as pd
import numpy as np
from math import *

%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.cm as cm

import torch.utils.data as Data

from torch.autograd import Variable

import csv

导入训练数据,可以使用train.head()查看导入的结果,便于后续的处理。

1
train = pd.read_csv("train.csv")

对数据进行处理,由于要使用的是CNN,我们必须要把数据整理成能输入的形式,即从数组变成高维张量。

1
2
3
4
5
train_labels = torch.from_numpy(np.array(train.label[:]))

image_size = train.iloc[:, 1:].shape[1]
image_width = image_height = np.ceil(np.sqrt(image_size)).astype(np.uint8)
train_data = torch.FloatTensor(np.array(train.iloc[:, 1:]).reshape((-1, 1, image_width, image_height))) / 255 #灰度压缩,进行归一化

注:reshape中的-1表示自适应,这样我们能让我们更好的变化数据的形式。

我们可以使用matplotlib查看数据处理的结果。

1
2
3
plt.imshow(train_data[1].numpy().squeeze(), cmap = 'gray')
plt.title('%i' % train_labels[1])
plt.show()

可以看到如下图片,可以与plt.title进行核对。

注:可以用squeeze()函数来降维,例如:从[[1]]—>[1]
与之相反的是便是unsqueeze(dim = 1),该函数可以使[1]—>[[1]]

以同样的方式导入并处理测试集。

1
2
test= pd.read_csv("test.csv")
test_data = torch.FloatTensor(np.array(test).reshape((-1, 1, image_width, image_height))) / 255

接下来我们定义几个超参数,这里将要使用的是小批梯度下降的优化算法,因此定义如下:

1
2
3
4
#超参数
EPOCH = 1 #整个数据集循环训练的轮数
BATCH_SIZE = 10 #每批的样本个数
LR = 0.01 #学习率

定义好超参数之后,我们使用Data对数据进行最后的处理。

1
2
3
4
5
6
7
trainData = Data.TensorDataset(train_data, train_labels) #用后会变成元组类型

train_loader = Data.DataLoader(
dataset = trainData,
batch_size = BATCH_SIZE,
shuffle = True
)

上面的Data.TensorDataset可以把数据进行打包,以方便我们更好的使用;而Data.DataLoade可以将我们的数据打乱并且分批。要注意的是,这里不要对测试集进行操作,否则最终输出的结果就难以再与原来的顺序匹配了。
接下来,我们定义卷积神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#build CNN
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
#一个卷积层
self.conv1 = nn.Sequential(
nn.Conv2d( #输入(1, 28, 28)
in_channels = 1, #1个通道
out_channels = 16, #输出层数
kernel_size = 5, #过滤器的大小
stride = 1, #步长
padding = 2 #填白
), #输出(16, 28, 28)
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
)
self.conv2 = nn.Sequential( #输入(16, 14, 14)
nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
)
self.out = nn.Linear(32 * 7 * 7, 10) #全连接层,将三维的数据展为2维的数据并输出

def forward(self, x): #父类已定义,不能修改名字
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
output = F.softmax(self.out(x))
return output

cnn = CNN()
optimzer = torch.optim.Adam(cnn.parameters(), lr = LR) #define optimezer
loss_func = nn.CrossEntropyLoss() #loss function使用交叉嫡误差

print(cnn) # 查看net architecture

完成以上的操作之后,就可以开始训练了,整个训练时间在CPU上只需要几分钟,这比KNN算法要优越许多。

1
2
3
4
5
6
7
8
9
10
11
12
for epoch in range(EPOCH):
for step, (x, y) in enumerate(train_loader):
b_x = Variable(x)
b_y = Variable(y)
output = cnn(b_x)
loss = loss_func(output, b_y) #cross entropy loss
#update W
optimzer.zero_grad()
loss.backward()
optimzer.step()
print('epoch%d' % (epoch + 1), '-', 'batch%d' % step, '-', 'loss%f' % loss) #查看训练过程
print('No.%depoch is over' % (epoch + 1))

代入测试集求解:

1
2
3
4
5
output = cnn(test_data[:])
#print(output)

result = torch.max(output, 1)[1].squeeze()
#print(result)

仿照KNN中的结果转存函数,定义saveResult函数。

1
2
3
4
5
6
7
def saveResult(result):
with open('result.csv', 'w') as myFile:
myWriter = csv.writer(myFile)
for i in result:
tmp = []
tmp.append(i)
myWriter.writerow(tmp)

最后使用saveResult(result.numpy())把结果存入csv文件。


改进

然而,若使用上述的CNN,得出的结果在leaderboard上会达到两千三百多名,这已经进入所有参赛者的倒数两百名之内了。为什么这个CNN的表现甚至不如我前面的KNN算法呢?我觉得主要有下面三个原因。

  1. 首先,由于CNN的参数较多,仅经过1轮epoch应该是不足够把所有参数训练到最优或者接近最优的位置的。个人认为,靠前的数据在参数相对远离最优值时参与训练而在之后不起作用,很有可能导致最后顾此失彼,因此有必要增加epoch使之前的数据多次参与参数的校正。同时,也要增大batch size使每次优化参数使用的样本更多,从而在测试集上表现更好。训练结束后,我发现我的C盘会被占用几个G,不知道是不是出错了,也有可能是参数占用的空间,必须停止kernel才能得到释放(我关闭了VScode后刷新,空间就回来了)。关于内存,这里似乎存在着一个问题,我将在后文阐述。

    注:由于VScode前段时间也开始支持ipynb,喜欢高端暗黑科技风又懒得自己修改jupyter notebook的小伙伴可以试一试。

  2. 学习率过大。尽管我这里的学习率设置为0.01,但对于最后的收敛来说或许还是偏大,这就导致了最后会在最优解附近来回抖动而难以接近的问题。关于这个问题,可以到deep-learning笔记:学习率衰减与批归一化中看看我较为详细的分析与解决方法。

  3. 由于训练时间和epoch轮数相对较小,我推测模型可能会存在过拟合的问题。尤其是最后的全连接层,它的结构很容易造成过拟合。关于这个问题,也可以到machine-learning笔记:过拟合与欠拟合machine-learning笔记:机器学习中正则化的理解中看看我较为详细的分析与解决方法。

针对上述原因,我对我的CNN模型做了如下调整:

  1. 首先,增加训练量,调整超参数如下。

    1
    2
    3
    4
    #超参数
    EOPCH = 3
    BATCH_SIZE = 50
    LR = 1e-4
  2. 引入dropout随机失活,加强全连接层的鲁棒性,修改网络结构如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #build CNN
    class CNN(nn.Module):
    def __init__(self):
    super(CNN, self).__init__()
    #一个卷积层
    self.conv1 = nn.Sequential(
    nn.Conv2d( #输入(1, 28, 28)
    in_channels = 1, #1个通道
    out_channels = 16, #输出层数
    kernel_size = 5, #过滤器的大小
    stride = 1, #步长
    padding = 2 #填白
    ), #输出(16, 28, 28)
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2), #输出(16, 14, 14)
    )
    self.conv2 = nn.Sequential( #输入(16, 14, 14)
    nn.Conv2d(16, 32, 5, 1, 2), #这里用了两个过滤器,将16层变成了32层
    nn.ReLU(),
    nn.MaxPool2d(kernel_size = 2) #输出(32, 7, 7)
    )

    self.dropout = nn.Dropout(p = 0.5) #每次减少50%神经元之间的连接

    self.fc = nn.Linear(32 * 7 * 7, 1024)
    self.out = nn.Linear(1024, 10) #全连接层,将三维的数据展为2维的数据并输出

    def forward(self, x):
    x = self.conv1(x)
    x = self.conv2(x)
    x = x.view(x.size(0), -1)
    x = self.fc(x)
    x = self.dropout(x)
    output = F.softmax(self.out(x))
    return output

本想直接使用torch.nn.functional中的dropout函数轻松实现随机失活正则化,但在网上看到这个函数好像有点坑,因此就不以身试坑了,还是在网络初始化中先定义dropout。

注:训练完新定义的网络之后我一直在思考dropout添加的方式与位置。在看了一些资料之后,我认为或许去掉全连接层、保持原来的层数并在softmax之前dropout可能能达到更好的效果。考虑到知乎上有知友提到做研究试验不宜在MNIST这些玩具级别的数据集上进行,因此暂时不再做没有太大意义的调整,今后有空在做改进试验。

经过上面的改进后,我再次训练网络并提交结果,在kaggle上的评分提高至0.97328,大约处在1600名左右,可以继续调整超参数(可以分割验证集寻找)和加深网络结构以取得更高的分数,但我的目的已经达到了。与之前的KNN相比,无论从时间效率还是准确率,CNN都有很大的进步,这也体现了深度学习相对于一些经典机器学习算法的优势。


出现的问题

由于这个最后的网络是我重复构建之后完成的,因此下列部分问题可能不存在于我上面的代码中,但我还是想汇总在这,以防之后继续踩相同的坑。

  1. 报错element 0 of tensors does not require grad and does not have a grad_fn

    pytorch具有自动求导机制,这就省去了我们编写反向传播的代码。每个Variable变量都有两个标志:requires_grad和volatile。出现上述问题的原因是requires_grad = False,修改或者增加(因为默认是false)成True即可。
  2. RuntimeError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

    这个好像是我在计算交叉熵时遇到的,原因是因为torch的交叉熵的输入第一个位置的输入应该是在每个label下的概率,而不是对应的label,详细分析与举例可参考文首给出的第三个链接。
  3. AttributeError: ‘tuple’ object has no attribute ‘numpy’

    为了查看数据处理效果,我在数据预处理过程中使用matplotlib绘制出处理后的图像,但是却出现了如上报错,当时的代码如下:

    1
    2
    3
    plt.imshow(trainData[1].numpy().squeeze(), cmap = 'gray')
    plt.title('%i' % train_labels[1])
    plt.show()

    查找相关资料之后,我才知道torch.utils.data会把打包的数据变成元组类型,因此我们绘图还是要使用原来train_data中的数据。

  4. 转存结果时提醒DefaultCPUAllocator: not enough memory

    由于当初在实现KNN算法转存结果时使用的函数存入csv文件后还要对文件进行空值删除处理,比较麻烦(后文会写具体如何处理),因此我想借用文章顶部给出的第二个链接中提供的方法:

    1
    2
    3
    out = pd.DataFrame(np.array(result), index = range(1, 1 + len(result)), columns = ['ImageId', 'Label'])
    #torch和pandas的类型不能直接的转换,所以需要借助numpy中间的步骤,将torch的数据转给pandas
    out.to_csv('result.csv', header = None)

    结果出现如下错误:

    我好歹也是八千多买的DELL旗舰本,8G内存,它居然说我不够让我换块新的RAM?什么情况…
    尝试许久,我怀疑是训练得到的参数占用了我的内存,那只好先把训练出的result保存下来,再导入到csv文件。
    最后我还是选择自己手动处理csv文件中的空值,应该有其它的转存csv文件的方法或者上述问题的解决措施,留待以后实践过程中发现解决,也欢迎大家不吝赐教。


excel/csv快速删除空白行

如果你使用的是我的saveResult函数或者类似,你就很有可能发现更新后的csv文件中数据之间双数行都是留空的,即一列数据之间都有空白行相隔,那么可以使用如下方法快速删除空白行。

  1. 选中对应列或者区域。
  2. 在“开始”工具栏中找到“查找与选择”功能并点击。
  3. 在下拉菜单中,点击“定位条件”选项。
  4. 在打开的定位条件窗口中,选择“空值”并确定。
  5. 待电脑为你选中所有空值后,任意右键一个被选中的空白行,在弹出的菜单中点击“删除”。
  6. 如果数据量比较大,这时候会有一个处理时间可能会比较长的提醒弹出,确认即可。
  7. 等待处理完毕。

碰到底线咯 后面没有啦

本文标题:kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集

文章作者:高深远

发布时间:2019年11月02日 - 11:24

最后更新:2020年02月07日 - 17:22

原始链接:https://gsy00517.github.io/kaggle20191102112435/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
linear algebra笔记:二维仿射变换 | 高深远的博客

linear algebra笔记:二维仿射变换

之前在deep-learning笔记:学习率衰减与批归一化一文中,我已经对仿射变换作了简单的介绍。但这里我想提出来单独对其做一个小归纳。


应用

仿射变换在计算机科学中有丰富的运用。例如,在计算机图形学中,它可以用于在较小或较大的屏幕上显示图形内容时简单地重新缩放图形内容。此外,它也可以应用于扭曲一个图像到另一个图像平面。
另一个重要的应用是训练深层神经网络时用于扩充数据集。训练深度模型需要大量的数据。在几乎所有的情况下,模型都受益于更高的泛化性能,因为有更多的训练图像。人工生成更多数据的一种方法就是对输入数据随机应用仿射变换(数据增强)。
此外,在模型要求固定尺寸的输入时,仿射变换也是一种主要的解决方案。


仿射变换

二维仿射变换可以用下面这个公式来表示:

其中$A$是在齐次坐标系中的3x3矩阵,$x$是在齐次坐标系中$(x,y,1)$形式的向量。这个公式表示$A$将一个任意向量$x$映射到另一个向量$x’$。
一般来说,仿射变换有6个自由度。根据参数的值,它将在矩阵乘法后扭曲任何图像。变换后的图像保留了原始图像中的平行直线(考虑剪切)。本质上,满足这两个条件的任何变换都是仿射的。它保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点可以确定一个唯一的仿射变换。
下面一些特殊形式的$A$,如下图所示,从上到下分别是:缩放、平移和旋转。

上述仿射变换的一个非常有用的性质是它们是线性函数。它们保留了乘法和加法运算,并遵循叠加原理。

换言之,我们可以组合2个或更多的变换:向量加法表示平移,矩阵乘法表示线性映射,只要我们用齐次坐标表示它们。即利用这个性质,我们可以将二维仿射变换视为线性变换R和平移变换T的叠加,具体可以看一下之前的文章。
举个例子,我们可以将旋转和平移如下表示:

1
2
3
A = array([[cos(angle), -sin(angle), tx],
[sin(angle), cos(angle), ty],
[0, 0, 1]])

如果理解了的话,你会发现各种各样的变化其实还挺繁琐的。不过请放心,大多数开发人员和研究人员通常省去了编写所有这些变换的麻烦,而只需依赖优化的库来执行任务。在OpenCV中进行仿射变换非常简单,如果今后遇到的比较多再做整理。


分享

之前在artificial-intelligence笔记:吴恩达——阅读论文的建议一文中提到以后会分享几个觉得有价值的公众号的,这里就再分享一个吧。本文中的图片均来自该公众号的推文。


碰到底线咯 后面没有啦

本文标题:linear algebra笔记:二维仿射变换

文章作者:高深远

发布时间:2020年01月16日 - 08:47

最后更新:2020年02月01日 - 21:56

原始链接:https://gsy00517.github.io/linear-algebra20200116084728/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
linear algebra笔记:二维仿射变换 | 高深远的博客

linear algebra笔记:二维仿射变换

之前在deep-learning笔记:学习率衰减与批归一化一文中,我已经对仿射变换作了简单的介绍。但这里我想提出来单独对其做一个小归纳。


应用

仿射变换在计算机科学中有丰富的运用。例如,在计算机图形学中,它可以用于在较小或较大的屏幕上显示图形内容时简单地重新缩放图形内容。此外,它也可以应用于扭曲一个图像到另一个图像平面。
另一个重要的应用是训练深层神经网络时用于扩充数据集。训练深度模型需要大量的数据。在几乎所有的情况下,模型都受益于更高的泛化性能,因为有更多的训练图像。人工生成更多数据的一种方法就是对输入数据随机应用仿射变换(数据增强)。
此外,在模型要求固定尺寸的输入时,仿射变换也是一种主要的解决方案。


仿射变换

二维仿射变换可以用下面这个公式来表示:

其中$A$是在齐次坐标系中的3x3矩阵,$x$是在齐次坐标系中$(x,y,1)$形式的向量。这个公式表示$A$将一个任意向量$x$映射到另一个向量$x’$。
一般来说,仿射变换有6个自由度。根据参数的值,它将在矩阵乘法后扭曲任何图像。变换后的图像保留了原始图像中的平行直线(考虑剪切)。本质上,满足这两个条件的任何变换都是仿射的。它保持了二维图形的“平直性”、“平行性”和“共线比例不变性”,非共线的三对对应点可以确定一个唯一的仿射变换。
下面一些特殊形式的$A$,如下图所示,从上到下分别是:缩放、平移和旋转。

上述仿射变换的一个非常有用的性质是它们是线性函数。它们保留了乘法和加法运算,并遵循叠加原理。

换言之,我们可以组合2个或更多的变换:向量加法表示平移,矩阵乘法表示线性映射,只要我们用齐次坐标表示它们。即利用这个性质,我们可以将二维仿射变换视为线性变换R和平移变换T的叠加,具体可以看一下之前的文章。
举个例子,我们可以将旋转和平移如下表示:

1
2
3
A = array([[cos(angle), -sin(angle), tx],
[sin(angle), cos(angle), ty],
[0, 0, 1]])

如果理解了的话,你会发现各种各样的变化其实还挺繁琐的。不过请放心,大多数开发人员和研究人员通常省去了编写所有这些变换的麻烦,而只需依赖优化的库来执行任务。在OpenCV中进行仿射变换非常简单,如果今后遇到的比较多再做整理。


分享

之前在artificial-intelligence笔记:吴恩达——阅读论文的建议一文中提到以后会分享几个觉得有价值的公众号的,这里就再分享一个吧。本文中的图片均来自该公众号的推文。


碰到底线咯 后面没有啦

本文标题:linear algebra笔记:二维仿射变换

文章作者:高深远

发布时间:2020年01月16日 - 08:47

最后更新:2020年02月01日 - 21:56

原始链接:https://gsy00517.github.io/linear-algebra20200116084728/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
linear algebra笔记:循环矩阵 | 高深远的博客

linear algebra笔记:循环矩阵

循环矩阵是我在看相关滤波时遇到的一个terminology,通过一定的了解之后发现其具有许多有用的性质。在目标跟踪领域,循环矩阵的引入对速度的提升是非常大的。关于相关滤波,由于现在了解还不够全面和深入,暂时不提及。本文主要就循环矩阵的概念和性质做一个总结。

References

电子文献:
https://www.cnblogs.com/cj-xxz/p/10323711.html
https://blog.csdn.net/shenxiaolu1984/article/details/50884830

参考文献:
[1]High-Speed Tracking with Kernelized Correlation Filters


循环矩阵(Circulant Matrices)

任意循环矩阵可以被傅里叶变换矩阵对角化。(All circulant matrices are made diagonal by the Discrete Fourier Transform (DFT), regardless of the generating vector x.)我们在文献中往往会看到这样一个变换:

下面的$X$它就是一个循环矩阵,它是由它的第一行$x=(x_{1},x_{2},…,x_{n})$的向量组每次经过一个循环移位,得到的一个循环矩阵。其中$\widehat{x}$(读作x hat)为原向量$x$的傅里叶变换;$F$是傅里叶变换矩阵,$F^{H}$表示共轭转置。
换句话说,循环矩阵$X$相似于对角阵,其特征值是$\widehat{x}$的元素。以长度为3的$x$为例,其生成的循环矩阵为:

这样的一个矩阵它有一个特别好的性质,就是能够通过它的第一行的生成向量来做来进行对角化。通过这个式子,我们能够把$X$循环矩阵进行对角化,如此把它转换到傅里叶域之后,用离散傅里叶变化来做运算时,对速度的提升是非常大的,这将在后文进一步说明。

关于这里的对角化、傅里叶变换矩阵可以看后文,在此先跳过。

这里有必要列出循环矩阵的两个重要公式,这两个性质是比较常用和有用的:

  1. 卷积

    循环矩阵乘向量等价于生成向量的逆序和该向量卷积,可进一步转化为傅里叶变换相乘。 这里$\overline{x}$表示$x$的逆序排列,$*$表示共轭。

    注意:卷积本身也包含逆序操作。此外,这里最后一个等号利用了信号与系统中的“时域卷积,频域相乘”,即时域卷积定理,它表明两信号在时域的卷积积分对应于在频域中该两信号的傅里叶变换的乘积。

  2. 相乘

    循环矩阵的乘积仍是循环矩阵,所以我们只要维护循环矩阵的第一行,就可以以较低的复杂度维护循环矩阵的乘积。 公式中最终所得的乘积也是循环矩阵,其生成向量是原生成向量对位相乘的傅里叶逆变换。

用了上述循环矩阵的性质之后,我们就可以使得原来两个矩阵相乘的时间复杂度$O(K^{3})$能够降到$O(Klog(K))$(反向傅里叶的复杂度($O(Klog(K))$)加上向量点乘的复杂度($K$)),速度的提升是非常明显的。

注:这里K表示的是矩阵的尺寸。

在非线形的情况下,当引入了核之后,也可以得到同样的一个情况。此时需要这个核满足一定的条件,它是可以具备循环矩阵的一些性质的,例如常用的高斯核、线性核都满足这个条件,因此可以直接拿来用。


傅里叶变换矩阵(DFT matrix)

关于离散傅里叶矩阵$F$这里涉及较多的数学,想看详细推导可以参考文首给出的第二个参考链接。这里把比较关键的结论部分截了过来。

$F$在这里是一个奇异矩阵(方阵且行列式等于零),它可以对任意输入向量进行傅里叶变换,这是因为傅里叶变换具有线性性。


矩阵快速幂

在本文前的第一个参考链接中,作者是用矩阵快速幂引入的,那么这里我也简单谈一下快速幂。
顾名思义,快速幂就是快速算某个数的多少次幂。
我们知道,对于任何一个整数,都能用二进制来表示。那么对于$a^{n}$,$n$也一定可以用二进制来表示。
那么问题来了,如何计算某个数较大的次幂呢?
比如计算$a^{156}$,我们可以利用除二取余、倒序排列、高位补零的方法得到$(156)_{10}=(10011100)_{2}$。
如此可以推导:

这样一来,原本要进行$156-1=155$次乘法运算,现在运算量级相当于该幂的二进制数表示中1的个数。其时间复杂度为$O(log_{2}n)$,与朴素的$O(n)$相比,效率有了极大地提高。
以上就是一般的快速幂的基本套路。相对于一般的快速幂,矩阵快速幂仅仅是把他的底数和乘数换成了矩阵形式。其主要方法就是:通过把数放到矩阵的不同位置,然后把普通递推式构造成类似于“矩阵的等比数列”,最后快速幂求解递推式。
矩阵快速幂主要用于求一个很复杂的递推式中的某一项问题。
递推矩阵(关系矩阵)的构造,也是矩阵快速幂的难点,一般是由原始的递推公式推导或者配凑得出,网上有许多ACM的赛题解答,可以看几道理解一下思路。


碰到底线咯 后面没有啦

本文标题:linear algebra笔记:循环矩阵

文章作者:高深远

发布时间:2020年01月16日 - 09:57

最后更新:2020年03月12日 - 10:12

原始链接:https://gsy00517.github.io/linear-algebra20200116095725/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
linear algebra笔记:循环矩阵 | 高深远的博客

linear algebra笔记:循环矩阵

循环矩阵是我在看相关滤波时遇到的一个terminology,通过一定的了解之后发现其具有许多有用的性质。在目标跟踪领域,循环矩阵的引入对速度的提升是非常大的。关于相关滤波,由于现在了解还不够全面和深入,暂时不提及。本文主要就循环矩阵的概念和性质做一个总结。

References

电子文献:
https://www.cnblogs.com/cj-xxz/p/10323711.html
https://blog.csdn.net/shenxiaolu1984/article/details/50884830

参考文献:
[1]High-Speed Tracking with Kernelized Correlation Filters


循环矩阵(Circulant Matrices)

任意循环矩阵可以被傅里叶变换矩阵对角化。(All circulant matrices are made diagonal by the Discrete Fourier Transform (DFT), regardless of the generating vector x.)我们在文献中往往会看到这样一个变换:

下面的$X$它就是一个循环矩阵,它是由它的第一行$x=(x_{1},x_{2},…,x_{n})$的向量组每次经过一个循环移位,得到的一个循环矩阵。其中$\widehat{x}$(读作x hat)为原向量$x$的傅里叶变换;$F$是傅里叶变换矩阵,$F^{H}$表示共轭转置。
换句话说,循环矩阵$X$相似于对角阵,其特征值是$\widehat{x}$的元素。以长度为3的$x$为例,其生成的循环矩阵为:

这样的一个矩阵它有一个特别好的性质,就是能够通过它的第一行的生成向量来做来进行对角化。通过这个式子,我们能够把$X$循环矩阵进行对角化,如此把它转换到傅里叶域之后,用离散傅里叶变化来做运算时,对速度的提升是非常大的,这将在后文进一步说明。

关于这里的对角化、傅里叶变换矩阵可以看后文,在此先跳过。

这里有必要列出循环矩阵的两个重要公式,这两个性质是比较常用和有用的:

  1. 卷积

    循环矩阵乘向量等价于生成向量的逆序和该向量卷积,可进一步转化为傅里叶变换相乘。 这里$\overline{x}$表示$x$的逆序排列,$*$表示共轭。

    注意:卷积本身也包含逆序操作。此外,这里最后一个等号利用了信号与系统中的“时域卷积,频域相乘”,即时域卷积定理,它表明两信号在时域的卷积积分对应于在频域中该两信号的傅里叶变换的乘积。

  2. 相乘

    循环矩阵的乘积仍是循环矩阵,所以我们只要维护循环矩阵的第一行,就可以以较低的复杂度维护循环矩阵的乘积。 公式中最终所得的乘积也是循环矩阵,其生成向量是原生成向量对位相乘的傅里叶逆变换。

用了上述循环矩阵的性质之后,我们就可以使得原来两个矩阵相乘的时间复杂度$O(K^{3})$能够降到$O(Klog(K))$(反向傅里叶的复杂度($O(Klog(K))$)加上向量点乘的复杂度($K$)),速度的提升是非常明显的。

注:这里K表示的是矩阵的尺寸。

在非线形的情况下,当引入了核之后,也可以得到同样的一个情况。此时需要这个核满足一定的条件,它是可以具备循环矩阵的一些性质的,例如常用的高斯核、线性核都满足这个条件,因此可以直接拿来用。


傅里叶变换矩阵(DFT matrix)

关于离散傅里叶矩阵$F$这里涉及较多的数学,想看详细推导可以参考文首给出的第二个参考链接。这里把比较关键的结论部分截了过来。

$F$在这里是一个奇异矩阵(方阵且行列式等于零),它可以对任意输入向量进行傅里叶变换,这是因为傅里叶变换具有线性性。


矩阵快速幂

在本文前的第一个参考链接中,作者是用矩阵快速幂引入的,那么这里我也简单谈一下快速幂。
顾名思义,快速幂就是快速算某个数的多少次幂。
我们知道,对于任何一个整数,都能用二进制来表示。那么对于$a^{n}$,$n$也一定可以用二进制来表示。
那么问题来了,如何计算某个数较大的次幂呢?
比如计算$a^{156}$,我们可以利用除二取余、倒序排列、高位补零的方法得到$(156)_{10}=(10011100)_{2}$。
如此可以推导:

这样一来,原本要进行$156-1=155$次乘法运算,现在运算量级相当于该幂的二进制数表示中1的个数。其时间复杂度为$O(log_{2}n)$,与朴素的$O(n)$相比,效率有了极大地提高。
以上就是一般的快速幂的基本套路。相对于一般的快速幂,矩阵快速幂仅仅是把他的底数和乘数换成了矩阵形式。其主要方法就是:通过把数放到矩阵的不同位置,然后把普通递推式构造成类似于“矩阵的等比数列”,最后快速幂求解递推式。
矩阵快速幂主要用于求一个很复杂的递推式中的某一项问题。
递推矩阵(关系矩阵)的构造,也是矩阵快速幂的难点,一般是由原始的递推公式推导或者配凑得出,网上有许多ACM的赛题解答,可以看几道理解一下思路。


碰到底线咯 后面没有啦

本文标题:linear algebra笔记:循环矩阵

文章作者:高深远

发布时间:2020年01月16日 - 09:57

最后更新:2020年03月12日 - 10:12

原始链接:https://gsy00517.github.io/linear-algebra20200116095725/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
logisim笔记:基本使用及运动码表的实现 | 高深远的博客

logisim笔记:基本使用及运动码表的实现

似乎好久没有写新的博文了。由于今年的春节较往年要早,近一个月来,各门考试接踵而至让我忙得不可开交。虽然非常赶,终于还是考完了。当初想都不敢想的13天考10门的考试周也就这样熬过去了。事实证明,即便是很大的困难,逼一下自己还是能挺过来的。不过最后还是和往年的冬天一样发了个一年一度的高烧,真的难受,以后还得更加注重自己的身体。
废话不多说,这几天就打算先把这学期期末阶段一些有意义、有价值的的知识与经历整理记录一下。首先是一个运动码表的大作业,本文主要是使用Logisim对运动码表进行实现的方法与过程。


需求分析

该项目要求我们设计一个简单的运动码表,包括四个按键组成的输入模块和四个7段数码管组成的输出模块。四个按键分别是:Start,按下时计时器归零并重新开始计时;Stop,按下时停止计时并显示计时数据;Store,按下时尝试更新系统记录,要求记录当前码表值,若已有记录,则和当前待记录的值作对比,如果计时数据比记录数据要小即用时要短,则更新系统记录并显示;Reset,按下时进行复位,将计时数据置为00.00,系统记录置为99.99。


Logisim使用

在具体说明实现的方法之前,我想先把之前整理的一些Logisim的基本使用做一个简短的总结。Logisim的使用其实不难,可以参考网上整理的Logisim文档翻译就可以快速入门。实际上,在具备一定数字电路知识的情况下,到处点点也能够学会Logisim的基本使用方法。
Logisim为数字电路的设计提供了很大的帮助,我们可以通过填写真值表或者输入逻辑函数快速生成原本手动连线根本无法完成的复杂电路。
如果想要获得Logisim最新的优化版本,可以加入华科谭志虎教授创建的计算机硬件系统设计交流群(群号:957283876),此群汇集了多个高校的学生,是一个比较活跃的技术交流群,谭教授秒回且超赞的解答真的忍不住让人点赞!下面是一些我当初刚使用时记录在note上的一些Logisim的使用与常见处理,分享在此。

  1. 引脚

    在Logisim中,引脚即Pin,可以使用键盘上、下、左、右光标键来改变引脚的朝向。

    注意:这里该表朝向不是按照旋转方向来的,而是按哪个方向就是指向哪个方向。

    此外,根据形状的不同,引脚分为输入引脚(较方)和输出(较圆)引脚,可以使用UI左上角最左侧的手型的戳工具点击对应的引脚来修改引脚的值(高电平、低电平、未知x态)。
    当我们选中引脚时,按下alt+数字组合,就可以改变到对应的位宽。

  2. 与门

    选中与门,按下数字键,就修改输入引脚个数。
  3. 线颜色的含义

    • 亮绿色:高电平。
    • 深绿色:低电平。
    • 蓝色:未知状态。
    • 灰色:飞线。
    • 红色:信号冲突。
    • 黑色:多位总线。
    • 橙色:位宽不匹配。
  4. 时钟

    • ctrl+K:驱动与关闭时钟连续运行。
    • ctrl+T:驱动时钟单步运行。
  5. 其它快捷键

    上面列出的都是一些并比较典型的特例,别的地方使用基本类似,可以多多尝试。下面再列出两个比较常用的快捷键:
    • ctrl+D:创建副本。
    • ctrl+Z:撤销。

整体设计

为了实现运动码表的功能,我们将整个项目拆分成如下几个模块:

  • 计时模块(包括计时使能模块)。
  • 码表驱动与显示模块。
  • 系统记录数据寄存模块。
  • 码表状态功能控制模块。
  • 数值比较模块。
  • 数值选择模块。

此外,根据需求分析,我们设计了如下6个状态,并构建状态机如下:

  • 000 中间态:每次按键弹起后,回到该状态。
  • 101 复位:计时变成00.00,记录变成99.99,显示计时数值,时钟不计时。
  • 001 计时:计时变成00.00,记录不变,显示计时数值,时钟计时。
  • 010 停止:计时不变,记录不变,显示计时数值,时钟不计时。
  • 011 更新(小于系统记录NewRecord=1):计时不变,记录变成计时,显示记录数值,时钟不计时。
  • 100 不更新(大于等于系统记录NewRecord=0):计时不变,记录不变,显示记录数值,时钟不计时。

可得状态转换关系的真值表如下所示:

为了实现上述状态转换与控制信号输出,我们设计了如下码表控制电路。

其中SD-EN控制寄存器使能,DP-SEL控制码表显示数值的选择,TM-Reset控制是否复位。


设计实现

这是设计完成后最终circ文件中所包含的内容,下面我简述一下各个模块中的内容及思路。
数码管驱动:即将4位2进制数转化成7个二进制信号,驱动7段数码管的亮灭。
4位BCD计数器:基于下面的BCD计数器状态转换(即在0-9之间递增循环)和BCD计数器输出函数(即在达到9时输出进位信号)。
码表计数器:由四个4位BCD计数器组成。
码表显示驱动:即将四个4位二进制组成的数字通过四个7段数码管显示出来,内部基于上面的显示驱动转换电路即数码管驱动的封装。
码表控制器:上文的设计中已经提及,基于码表控制器状态转换(输入信号+现态->次态)和码表控制器输出函数(现态->控制信号)。
计时使能:这里我比较巧妙地运用了D触发器的置位清零两个端,将在后文提及。
最后就是运动码表的总电路图:


遇到的问题

  1. 复位后自动开始计时

    这是最让我头疼的一个问题,由于上述状态机的设计方法和Logisim中按键在鼠标松开后自动弹起的功能,导致在我按下Reset清零后系统会自动重新开始计时(因为这时的现态已经不是复位状态),这显然是不符合需求的。为了使计时使能在下一次按键到来之前能保持现态,我将复位控制信号从状态转换电路中单独取出,并使用一个D触发器的置位清零两个端子实现了这个保持功能,效果不错。
  2. 计时器遇到9时跳跃进位

    当最初的电路实现完成后,我兴奋地开始计时,结果计时器的花式进位方式顿时让我傻了眼。仔细研究后我发现,我起初设计的电路在进位时只考虑了低一位计数器传来的进位信号,而事实上,高位的进位条件往往是由后几位共同决定的,为此,修改电路如下: 问题解决。
  3. 码表在更新数据的前一个时钟内会显示较大的记录数值

    这个问题其实不是非常重要,但同班的一位同学还是注意到了这点。也就是说,当计时数据比系统记录要小的时候,系统应该更新记录并显示最好的成绩,然而当按下Store进行存储更新时,在前一个时钟周期内,码表会短暂地先显示原本较大即较差地系统记录,这是不希望出现的。
    解决的办法可以在寄存器和显示选择器之间添加一个三态门,具体可以看上文中的总电路图。

碰到底线咯 后面没有啦

本文标题:logisim笔记:基本使用及运动码表的实现

文章作者:高深远

发布时间:2020年01月07日 - 12:11

最后更新:2020年01月07日 - 22:29

原始链接:https://gsy00517.github.io/logisim20200107121101/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
logisim笔记:基本使用及运动码表的实现 | 高深远的博客

logisim笔记:基本使用及运动码表的实现

似乎好久没有写新的博文了。由于今年的春节较往年要早,近一个月来,各门考试接踵而至让我忙得不可开交。虽然非常赶,终于还是考完了。当初想都不敢想的13天考10门的考试周也就这样熬过去了。事实证明,即便是很大的困难,逼一下自己还是能挺过来的。不过最后还是和往年的冬天一样发了个一年一度的高烧,真的难受,以后还得更加注重自己的身体。
废话不多说,这几天就打算先把这学期期末阶段一些有意义、有价值的的知识与经历整理记录一下。首先是一个运动码表的大作业,本文主要是使用Logisim对运动码表进行实现的方法与过程。


需求分析

该项目要求我们设计一个简单的运动码表,包括四个按键组成的输入模块和四个7段数码管组成的输出模块。四个按键分别是:Start,按下时计时器归零并重新开始计时;Stop,按下时停止计时并显示计时数据;Store,按下时尝试更新系统记录,要求记录当前码表值,若已有记录,则和当前待记录的值作对比,如果计时数据比记录数据要小即用时要短,则更新系统记录并显示;Reset,按下时进行复位,将计时数据置为00.00,系统记录置为99.99。


Logisim使用

在具体说明实现的方法之前,我想先把之前整理的一些Logisim的基本使用做一个简短的总结。Logisim的使用其实不难,可以参考网上整理的Logisim文档翻译就可以快速入门。实际上,在具备一定数字电路知识的情况下,到处点点也能够学会Logisim的基本使用方法。
Logisim为数字电路的设计提供了很大的帮助,我们可以通过填写真值表或者输入逻辑函数快速生成原本手动连线根本无法完成的复杂电路。
如果想要获得Logisim最新的优化版本,可以加入华科谭志虎教授创建的计算机硬件系统设计交流群(群号:957283876),此群汇集了多个高校的学生,是一个比较活跃的技术交流群,谭教授秒回且超赞的解答真的忍不住让人点赞!下面是一些我当初刚使用时记录在note上的一些Logisim的使用与常见处理,分享在此。

  1. 引脚

    在Logisim中,引脚即Pin,可以使用键盘上、下、左、右光标键来改变引脚的朝向。

    注意:这里该表朝向不是按照旋转方向来的,而是按哪个方向就是指向哪个方向。

    此外,根据形状的不同,引脚分为输入引脚(较方)和输出(较圆)引脚,可以使用UI左上角最左侧的手型的戳工具点击对应的引脚来修改引脚的值(高电平、低电平、未知x态)。
    当我们选中引脚时,按下alt+数字组合,就可以改变到对应的位宽。

  2. 与门

    选中与门,按下数字键,就修改输入引脚个数。
  3. 线颜色的含义

    • 亮绿色:高电平。
    • 深绿色:低电平。
    • 蓝色:未知状态。
    • 灰色:飞线。
    • 红色:信号冲突。
    • 黑色:多位总线。
    • 橙色:位宽不匹配。
  4. 时钟

    • ctrl+K:驱动与关闭时钟连续运行。
    • ctrl+T:驱动时钟单步运行。
  5. 其它快捷键

    上面列出的都是一些并比较典型的特例,别的地方使用基本类似,可以多多尝试。下面再列出两个比较常用的快捷键:
    • ctrl+D:创建副本。
    • ctrl+Z:撤销。

整体设计

为了实现运动码表的功能,我们将整个项目拆分成如下几个模块:

  • 计时模块(包括计时使能模块)。
  • 码表驱动与显示模块。
  • 系统记录数据寄存模块。
  • 码表状态功能控制模块。
  • 数值比较模块。
  • 数值选择模块。

此外,根据需求分析,我们设计了如下6个状态,并构建状态机如下:

  • 000 中间态:每次按键弹起后,回到该状态。
  • 101 复位:计时变成00.00,记录变成99.99,显示计时数值,时钟不计时。
  • 001 计时:计时变成00.00,记录不变,显示计时数值,时钟计时。
  • 010 停止:计时不变,记录不变,显示计时数值,时钟不计时。
  • 011 更新(小于系统记录NewRecord=1):计时不变,记录变成计时,显示记录数值,时钟不计时。
  • 100 不更新(大于等于系统记录NewRecord=0):计时不变,记录不变,显示记录数值,时钟不计时。

可得状态转换关系的真值表如下所示:

为了实现上述状态转换与控制信号输出,我们设计了如下码表控制电路。

其中SD-EN控制寄存器使能,DP-SEL控制码表显示数值的选择,TM-Reset控制是否复位。


设计实现

这是设计完成后最终circ文件中所包含的内容,下面我简述一下各个模块中的内容及思路。
数码管驱动:即将4位2进制数转化成7个二进制信号,驱动7段数码管的亮灭。
4位BCD计数器:基于下面的BCD计数器状态转换(即在0-9之间递增循环)和BCD计数器输出函数(即在达到9时输出进位信号)。
码表计数器:由四个4位BCD计数器组成。
码表显示驱动:即将四个4位二进制组成的数字通过四个7段数码管显示出来,内部基于上面的显示驱动转换电路即数码管驱动的封装。
码表控制器:上文的设计中已经提及,基于码表控制器状态转换(输入信号+现态->次态)和码表控制器输出函数(现态->控制信号)。
计时使能:这里我比较巧妙地运用了D触发器的置位清零两个端,将在后文提及。
最后就是运动码表的总电路图:


遇到的问题

  1. 复位后自动开始计时

    这是最让我头疼的一个问题,由于上述状态机的设计方法和Logisim中按键在鼠标松开后自动弹起的功能,导致在我按下Reset清零后系统会自动重新开始计时(因为这时的现态已经不是复位状态),这显然是不符合需求的。为了使计时使能在下一次按键到来之前能保持现态,我将复位控制信号从状态转换电路中单独取出,并使用一个D触发器的置位清零两个端子实现了这个保持功能,效果不错。
  2. 计时器遇到9时跳跃进位

    当最初的电路实现完成后,我兴奋地开始计时,结果计时器的花式进位方式顿时让我傻了眼。仔细研究后我发现,我起初设计的电路在进位时只考虑了低一位计数器传来的进位信号,而事实上,高位的进位条件往往是由后几位共同决定的,为此,修改电路如下: 问题解决。
  3. 码表在更新数据的前一个时钟内会显示较大的记录数值

    这个问题其实不是非常重要,但同班的一位同学还是注意到了这点。也就是说,当计时数据比系统记录要小的时候,系统应该更新记录并显示最好的成绩,然而当按下Store进行存储更新时,在前一个时钟周期内,码表会短暂地先显示原本较大即较差地系统记录,这是不希望出现的。
    解决的办法可以在寄存器和显示选择器之间添加一个三态门,具体可以看上文中的总电路图。

实现

看到评论区有许多同学似乎有一些困难无法解决,因此我在此提供了我最后实现的百度网盘链接,提取码为e8i1

注意:文件中包含logisim和运动码表的电路文件。我在当初实现时,一些模块是直接使用了库中已有的器件,并没有全部从最基础的器件开始实现。此外我的思路和华科谭志虎教授的慕课在细节上也有稍许不同,一些要点都已写在本篇文章中了。本人的实现仅供参考,如是学校作业,还需根据具体要求进行调整或者修改。


碰到底线咯 后面没有啦

本文标题:logisim笔记:基本使用及运动码表的实现

文章作者:高深远

发布时间:2020年01月07日 - 12:11

最后更新:2020年05月03日 - 09:38

原始链接:https://gsy00517.github.io/logisim20200107121101/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:机器学习中正则化的理解 | 高深远的博客

machine learning笔记:机器学习中正则化的理解

在接触了一些ML的知识后,大家一定会对正则化这个词不陌生,但是我感觉根据这个词的字面意思不能够直接地理解它的概念。因此我打算写一篇文章做个记录,方便以后回忆。

References

参考文献:
[1]统计学习方法(第2版)


线性代数中的正则化

如果直接搜索正则化这个名词,首先得到的一般是代数几何中的一个概念。
百度词条对它的解释是:给平面不可约代数曲线以某种形式的全纯参数表示。
怎么样?是不是觉得一头雾水。
这里我推荐使用谷歌或者维基百科来查询这些专业名词。

对于不能科学上网的朋友,没关系,我这里提供了谷歌镜像wikiwand,大家可以在上面得到一样的搜索结果。

我们直接到维基百科搜索regularization:
里面第一段是这样解释的:In mathematics, statistics, and computer science, particularly in machine learning and inverse problems, regularization is the process of adding information in order to solve an ill-posed problem or to prevent overfitting.
这就和我们在机器学习应用中的目的比较相近了。


机器学习中的正则化

在机器学习中,正则化是一种为了减小测试误差的行为(有时候会增加训练误差)。
我们在构造机器学习模型时,最终目的是让模型在面对新数据的时候,可以有很好的表现。当你用比较复杂的模型比如神经网络去拟合数据时,很容易出现过拟合现象(训练集表现很好,测试集表现较差),这会导致模型的泛化能力下降,这时候,我们就需要使用正则化,来降低模型的复杂度。
为了加深印象,我下面简单介绍几种常用的机器学习正则化方法:

  1. 早停法(Early Stopping)

    早停法,就是当训练集的误差变好,但是验证集的误差变坏(即泛化效果变差)的时候停止训练。这种方法可以一定程度上有效地防止过拟合,同时这也说明了验证集在机器学习中的重要性。
  2. 权重正则化法

    因为噪声相比于正常信号而言,通常会在某些点出现较大的峰值。所以,只要我们保证权重系数在绝对值意义上足够小,就能够保证噪声不会被过度响应,这也是奥卡姆剃刀原理的表现,即模型不应过度复杂,尤其是当数据量不大的时候。

    上面是在一个网课上看到的、我觉得可以较好地呈现模型的复杂度与数据量对模型预测表现的影响的一张图片,其中向左的横轴表示数据量大小,向右的横轴表示模型复杂度,竖轴是预测表现。通过这张图,可以很明显地观察到:模型的复杂度提升需要大量的数据作为依托。
    权重正则化主要有两种:

    • L1正则:$ J=J_{0}+\lambda \left | w \right |_{1} $,其中J代表损失函数(也称代价函数),$ \left | w \right |_{1} $代表参数向量w的L1范数。
    • L2正则(weight decay):$ J=J_{0}+\lambda \left | w \right |_{2} $,其中$ \left | w \right |_{2} $代表参数向量w的L2范数。 这里就产生了Lasso回归岭回归两大机器学习经典算法。其中Lasso回归是一种压缩估计,可以通过构造惩罚函数得到一个较为精炼的模型,使得它可以压缩一些系数,同时设定一些系数为0,从而达到特征选择的目的。基于Lasso回归这种可以选择特征并降维的特性,它主要有这些适用情况:
    1. 样本量比较小,但指标量非常多的时候(易于过拟合)。
    2. 进行高维统计时。
    3. 需要对特征进行选择时。
      对于这些回归的详细解释,大家可以到网上搜集相关信息。

      补充:
      L0范数:向量中非零元素的个数。
      L1范数:向量中每个元素绝对值的和。
      L2范数:向量元素绝对值的平方和再开方。

    下面我再附上一组图,希望能帮助更好地理解权重正则化:
    首先我们可视化一个损失函数。

    下面我们看一看正则化项的图示,这里使用L1范数作为正则化项。

    接着,我们将上面两者线性组合:

    我们来看看结果:

    可见,正则化项的引入排除了大量原本属于最优解的点,上图的情况中剩下一个唯一的局部最优解。
    正则化项的引入,除了符合奥卡姆剃刀原理之外。同时从贝叶斯估计的角度看,正则化项对应于模型的先验概率,即相当于假设复杂的模型具有较小的先验概率,而简单的模型具有较大的先验概率。

  3. 数据增强

    数据增强可以丰富图像数据集,有效防止过拟合。这种方法在AlexNet中有很好的应用,大家可以看看我的博文deep-learning笔记:开启深度学习热潮——AlexNet
  4. 随机失活(dropout)

    dropout即随机砍掉一部分神经元之间的连接,每次只更新一部分,这可以有效地增加它的鲁棒性,提高泛化能力。这个方法在AlexNet中也有详细的解释,推荐大家去看一下。 以上就是比较常规且流行的正则化方式,今后或许会有补充,也欢迎大家提供意见~

奥卡姆剃刀原理

上文在正则化一节提到了奥卡姆剃刀原理,这里就简单做个说明。
奥卡姆剃刀原理应用于模型选择时可以简单表述为如下思想:在所有可能选择的模型中,能够很好地解释已知数据并且十分简单的模型才是最好的模型,也就是我们应该选择的模型。


碰到底线咯 后面没有啦

本文标题:machine learning笔记:机器学习中正则化的理解

文章作者:高深远

发布时间:2019年09月15日 - 15:03

最后更新:2020年02月15日 - 08:09

原始链接:https://gsy00517.github.io/machine-learning20190915150339/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:机器学习中正则化的理解 | 高深远的博客

machine learning笔记:机器学习中正则化的理解

在接触了一些ML的知识后,大家一定会对正则化这个词不陌生,但是我感觉根据这个词的字面意思不能够直接地理解它的概念。因此我打算写一篇文章做个记录,方便以后回忆。

References

参考文献:
[1]统计学习方法(第2版)


线性代数中的正则化

如果直接搜索正则化这个名词,首先得到的一般是代数几何中的一个概念。
百度词条对它的解释是:给平面不可约代数曲线以某种形式的全纯参数表示。
怎么样?是不是觉得一头雾水。
这里我推荐使用谷歌或者维基百科来查询这些专业名词。

对于不能科学上网的朋友,没关系,我这里提供了谷歌镜像wikiwand,大家可以在上面得到一样的搜索结果。

我们直接到维基百科搜索regularization:
里面第一段是这样解释的:In mathematics, statistics, and computer science, particularly in machine learning and inverse problems, regularization is the process of adding information in order to solve an ill-posed problem or to prevent overfitting.
这就和我们在机器学习应用中的目的比较相近了。


机器学习中的正则化

在机器学习中,正则化是一种为了减小测试误差的行为(有时候会增加训练误差)。
我们在构造机器学习模型时,最终目的是让模型在面对新数据的时候,可以有很好的表现。当你用比较复杂的模型比如神经网络去拟合数据时,很容易出现过拟合现象(训练集表现很好,测试集表现较差),这会导致模型的泛化能力下降,这时候,我们就需要使用正则化,来降低模型的复杂度。
为了加深印象,我下面简单介绍几种常用的机器学习正则化方法:

  1. 早停法(Early Stopping)

    早停法,就是当训练集的误差变好,但是验证集的误差变坏(即泛化效果变差)的时候停止训练。这种方法可以一定程度上有效地防止过拟合,同时这也说明了验证集在机器学习中的重要性。
  2. 权重正则化法

    因为噪声相比于正常信号而言,通常会在某些点出现较大的峰值。所以,只要我们保证权重系数在绝对值意义上足够小,就能够保证噪声不会被过度响应,这也是奥卡姆剃刀原理的表现,即模型不应过度复杂,尤其是当数据量不大的时候。

    上面是在一个网课上看到的、我觉得可以较好地呈现模型的复杂度与数据量对模型预测表现的影响的一张图片,其中向左的横轴表示数据量大小,向右的横轴表示模型复杂度,竖轴是预测表现。通过这张图,可以很明显地观察到:模型的复杂度提升需要大量的数据作为依托。
    权重正则化主要有两种:

    • L1正则:$ J=J_{0}+\lambda \left | w \right |_{1} $,其中J代表损失函数(也称代价函数),$ \left | w \right |_{1} $代表参数向量w的L1范数。
    • L2正则(weight decay):$ J=J_{0}+\lambda \left | w \right |_{2} $,其中$ \left | w \right |_{2} $代表参数向量w的L2范数。 这里就产生了Lasso回归岭回归两大机器学习经典算法。其中Lasso回归是一种压缩估计,可以通过构造惩罚函数得到一个较为精炼的模型,使得它可以压缩一些系数,同时设定一些系数为0,从而达到特征选择的目的。基于Lasso回归这种可以选择特征并降维的特性,它主要有这些适用情况:
    1. 样本量比较小,但指标量非常多的时候(易于过拟合)。
    2. 进行高维统计时。
    3. 需要对特征进行选择时。
      对于这些回归的详细解释,大家可以到网上搜集相关信息。

      补充:
      L0范数:向量中非零元素的个数。
      L1范数:向量中每个元素绝对值的和。
      L2范数:向量元素绝对值的平方和再开方。

    下面我再附上一组图,希望能帮助更好地理解权重正则化:
    首先我们可视化一个损失函数。

    下面我们看一看正则化项的图示,这里使用L1范数作为正则化项。

    接着,我们将上面两者线性组合:

    我们来看看结果:

    可见,正则化项的引入排除了大量原本属于最优解的点,上图的情况中剩下一个唯一的局部最优解。
    正则化项的引入,除了符合奥卡姆剃刀原理之外。同时从贝叶斯估计的角度看,正则化项对应于模型的先验概率,即相当于假设复杂的模型具有较小的先验概率,而简单的模型具有较大的先验概率。

  3. 数据增强

    数据增强可以丰富图像数据集,有效防止过拟合。这种方法在AlexNet中有很好的应用,大家可以看看我的博文deep-learning笔记:开启深度学习热潮——AlexNet
  4. 随机失活(dropout)

    dropout即随机砍掉一部分神经元之间的连接,每次只更新一部分,这可以有效地增加它的鲁棒性,提高泛化能力。这个方法在AlexNet中也有详细的解释,推荐大家去看一下。 以上就是比较常规且流行的正则化方式,今后或许会有补充,也欢迎大家提供意见~

奥卡姆剃刀原理

上文在正则化一节提到了奥卡姆剃刀原理,这里就简单做个说明。
奥卡姆剃刀原理应用于模型选择时可以简单表述为如下思想:在所有可能选择的模型中,能够很好地解释已知数据并且十分简单的模型才是最好的模型,也就是我们应该选择的模型。


碰到底线咯 后面没有啦

本文标题:machine learning笔记:机器学习中正则化的理解

文章作者:高深远

发布时间:2019年09月15日 - 15:03

最后更新:2020年02月15日 - 08:09

原始链接:https://gsy00517.github.io/machine-learning20190915150339/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:一个支持向量机的问题 | 高深远的博客

machine learning笔记:一个支持向量机的问题

在学习机器学习理论的过程中,支持向量机(SVM)应该是我们会遇到的第一个对数学要求比较高的概念。理解它的原理要花费了我不少时间,写这篇博文是因为我之前看到的一个有关SVM的问题,其解答需用到SVM的相关数学原理,可以促使我思考。支持向量机的具体原理以及推导网上有大量资源,我也会在文中提供一些供参考。


简介

支持向量机是一种有监督的学习方法,主要思想是建立一个最优决策超平面,使得该平面两侧距离该平面最近的两类样本之间的距离最大化,从而对分类问题提供良好的泛化能力。
这里有个小故事,也是我第一次看SVM课程时老师提到的,可以通过这个小故事大致理解一下SVM在做什么。

它的优点主要有如下四点:
(1)相对于其他训练分类算法,SVM不需要过多的样本。
(2)SVM引入了核函数,可以处理高维的样本。
(3)结构风险最小。也就是说,分类器对问题真实模型的逼近与真实解之间的累计误差最小。
(4)由于SVM的非线性,它擅长应付线性不可分的问题。这主要是用松弛变量(惩罚变量)和核函数来实现的。
这里我附上我所知的三个SVM的常用软件工具包:SVMLightLibSVMLiblinear


问题

下面就是我在文章开头提到的问题,直接搬运:

解析中提到的拉格朗日乘子法和KKT条件,也是我在看到这个问题后才尝试去理解的。能力有限,不能自己很好的解释,这里附上瑞典皇家理工学院(KTH)“统计学习基础”课程的KKT课件,个人觉得讲的很直观且详细了。


碰到底线咯 后面没有啦

本文标题:machine learning笔记:一个支持向量机的问题

文章作者:高深远

发布时间:2019年10月01日 - 09:34

最后更新:2020年01月29日 - 13:05

原始链接:https://gsy00517.github.io/machine-learning20191001093428/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:一个支持向量机的问题 | 高深远的博客

machine learning笔记:一个支持向量机的问题

在学习机器学习理论的过程中,支持向量机(SVM)应该是我们会遇到的第一个对数学要求比较高的概念。理解它的原理要花费了我不少时间,写这篇博文是因为我之前看到的一个有关SVM的问题,其解答需用到SVM的相关数学原理,可以促使我思考。支持向量机的具体原理以及推导网上有大量资源,我也会在文中提供一些供参考。


简介

支持向量机是一种有监督的学习方法,主要思想是建立一个最优决策超平面,使得该平面两侧距离该平面最近的两类样本之间的距离最大化,从而对分类问题提供良好的泛化能力。
这里有个小故事,也是我第一次看SVM课程时老师提到的,可以通过这个小故事大致理解一下SVM在做什么。

它的优点主要有如下四点:
(1)相对于其他训练分类算法,SVM不需要过多的样本。
(2)SVM引入了核函数,可以处理高维的样本。
(3)结构风险最小。也就是说,分类器对问题真实模型的逼近与真实解之间的累计误差最小。
(4)由于SVM的非线性,它擅长应付线性不可分的问题。这主要是用松弛变量(惩罚变量)和核函数来实现的。
这里我附上我所知的三个SVM的常用软件工具包:SVMLightLibSVMLiblinear


问题

下面就是我在文章开头提到的问题,直接搬运:

解析中提到的拉格朗日乘子法和KKT条件,也是我在看到这个问题后才尝试去理解的。能力有限,不能自己很好的解释,这里附上瑞典皇家理工学院(KTH)“统计学习基础”课程的KKT课件,个人觉得讲的很直观且详细了。


碰到底线咯 后面没有啦

本文标题:machine learning笔记:一个支持向量机的问题

文章作者:高深远

发布时间:2019年10月01日 - 09:34

最后更新:2020年01月29日 - 13:05

原始链接:https://gsy00517.github.io/machine-learning20191001093428/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:过拟合与欠拟合 | 高深远的博客

machine learning笔记:过拟合与欠拟合

本文介绍在模型评估可能会出现的过拟合与欠拟合两种现象,并对解决方法做一个总结。


解释

我们先通过图片来直观地解释这两种现象:

在上图中,右边是过拟合的情况,它指的是模型对于训练数据拟合过度,反映到评估指标上,就是模型在训练集上的表现很好,但在测试集和新数据上的表现较差。这是因为在这种条件下,模型过于复杂,导致把噪声数据的特征也学习到了模型中,导致模型的泛化能力下降,从而在后期的应用过程中很容易输出错误的预测结果。
左边是欠拟合的情况,它指的是在训练和预测时的表现都不好,这样的模型没有很好地捕捉到数据地特征,从而不能够很好地拟合数据。
相比而言,中间是拟合适当的情况,这种模型在应用中就具有很好的鲁棒性。


解决方法

  1. 针对过拟合

    1. 获取更多数据

      更多的样本可以让模型学到更多有效的特征,从而减小噪声的影响。
      当然,一般情况下直接增加数据是很困难的,因此我们需要通过一定的规则来扩充训练数据。比如,在图像分类问题上,我们可以使用数据增强的方法,通过对图像的平移、旋转、缩放等方式来扩充数据;更进一步地,可以使用生成式对抗网络来合成大量新的训练数据。
    2. 降低模型复杂度

      模型复杂度过高是数据量较小时过拟合的主要原因。适当降低模型的复杂度可以避免模型拟合过多的噪声。比如,在神经网络模型中减少网络层数、神经元个数等;在决策树模型中降低树的深度、进行剪枝等。

      注意:网络深度增加引起的准确率退化不一定是过拟合引起的,这是因为深度造成的梯度消失、梯度爆炸等问题,这在ResNet的论文中有讨论,详细可以看我的博文deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现

    3. 正则化方法

      这里的方法主要是权重正则化法,具体说明可以参考machine-learning笔记:机器学习中正则化的理解
    4. 交叉验证

      交叉验证包括简单交叉验证(数据丰富时)、S折交叉验证(最常用)和留一交叉验证(数据匮乏时)。
    5. 集成学习

      即把多个模型集成在一起,从而降低单一模型的过拟合风险。主要有Bagging(bootstrap aggregating)和Boosting(adaptive boosting)这两种集成学习方法。
  2. 针对欠拟合

    解决欠拟合问题也可以参照解决过拟合问题的思路;
    1. 添加新特征

      当特征不足或者现有特征与样本标签的相关性不强时,模型容易出现欠拟合。
      因此,通过挖掘“上下文特征”、“组合特征”等新的特征,往往能够取得更好的效果。
      在深度学习中,也有很多模型可以帮助完成特征工程,比如因此分解机、梯度提升决策树、Deep-crossing等都可以成为丰富特征的方法。
    2. 增加模型复杂度

      当模型过于简单时,增加模型复杂度可以使模型拥有更强的拟合能力。比如,在线性模型中添加高次项,在神经网络模型中增加网络层数、神经元个数等。
      对于模型的选择,我在文末补充了两种模型选择的准则供参考。
    3. 减小正则化系数

      正则化是用来防止过拟合的,但当模型出现欠拟合现象时,我们就应该有针对性地减小正则化系数。

模型选择准则

模型选择的信息准则有很多,我这里介绍我知道的两个比较常用的模型选择准则:

  1. AIC准则

    赤池信息准则(Akaike Information Criterion,AIC)公式定义如下:其中k表示模型参数个数(复杂度),L表示经验误差(似然函数)。
    当需要从一组可供选择的模型中选择最佳模型时,通常选择AIC最小的模型。
  2. BIC准则

    贝叶斯信息准则(Bayesian Information Criterion,BIC)是对AIC准则的改进,定义如下:与AIC不同,这里k的系数不再是常数。其中n代表的是样本量(数据量),这样,BIC准则就与样本量相关了。当样本量足够时,过拟合的风险变小,我们就可以允许模型复杂一些。
    这里再次附上这张直观的图片,方便理解与体会。简析可参考machine-learning笔记:机器学习中正则化的理解

碰到底线咯 后面没有啦

本文标题:machine learning笔记:过拟合与欠拟合

文章作者:高深远

发布时间:2019年10月01日 - 10:45

最后更新:2020年01月30日 - 12:22

原始链接:https://gsy00517.github.io/machine-learning20191001104538/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:过拟合与欠拟合 | 高深远的博客

machine learning笔记:过拟合与欠拟合

本文介绍在模型评估可能会出现的过拟合与欠拟合两种现象,并对解决方法做一个总结。


解释

我们先通过图片来直观地解释这两种现象:

在上图中,右边是过拟合的情况,它指的是模型对于训练数据拟合过度,反映到评估指标上,就是模型在训练集上的表现很好,但在测试集和新数据上的表现较差。这是因为在这种条件下,模型过于复杂,导致把噪声数据的特征也学习到了模型中,导致模型的泛化能力下降,从而在后期的应用过程中很容易输出错误的预测结果。
左边是欠拟合的情况,它指的是在训练和预测时的表现都不好,这样的模型没有很好地捕捉到数据地特征,从而不能够很好地拟合数据。
相比而言,中间是拟合适当的情况,这种模型在应用中就具有很好的鲁棒性。


解决方法

  1. 针对过拟合

    1. 获取更多数据

      更多的样本可以让模型学到更多有效的特征,从而减小噪声的影响。
      当然,一般情况下直接增加数据是很困难的,因此我们需要通过一定的规则来扩充训练数据。比如,在图像分类问题上,我们可以使用数据增强的方法,通过对图像的平移、旋转、缩放等方式来扩充数据;更进一步地,可以使用生成式对抗网络来合成大量新的训练数据。
    2. 降低模型复杂度

      模型复杂度过高是数据量较小时过拟合的主要原因。适当降低模型的复杂度可以避免模型拟合过多的噪声。比如,在神经网络模型中减少网络层数、神经元个数等;在决策树模型中降低树的深度、进行剪枝等。

      注意:网络深度增加引起的准确率退化不一定是过拟合引起的,这是因为深度造成的梯度消失、梯度爆炸等问题,这在ResNet的论文中有讨论,详细可以看我的博文deep-learning笔记:使网络能够更深——ResNet简介与pytorch实现

    3. 正则化方法

      这里的方法主要是权重正则化法,具体说明可以参考machine-learning笔记:机器学习中正则化的理解
    4. 交叉验证

      交叉验证包括简单交叉验证(数据丰富时)、S折交叉验证(最常用)和留一交叉验证(数据匮乏时)。
    5. 集成学习

      即把多个模型集成在一起,从而降低单一模型的过拟合风险。主要有Bagging(bootstrap aggregating)和Boosting(adaptive boosting)这两种集成学习方法。
  2. 针对欠拟合

    解决欠拟合问题也可以参照解决过拟合问题的思路;
    1. 添加新特征

      当特征不足或者现有特征与样本标签的相关性不强时,模型容易出现欠拟合。
      因此,通过挖掘“上下文特征”、“组合特征”等新的特征,往往能够取得更好的效果。
      在深度学习中,也有很多模型可以帮助完成特征工程,比如因此分解机、梯度提升决策树、Deep-crossing等都可以成为丰富特征的方法。
    2. 增加模型复杂度

      当模型过于简单时,增加模型复杂度可以使模型拥有更强的拟合能力。比如,在线性模型中添加高次项,在神经网络模型中增加网络层数、神经元个数等。
      对于模型的选择,我在文末补充了两种模型选择的准则供参考。
    3. 减小正则化系数

      正则化是用来防止过拟合的,但当模型出现欠拟合现象时,我们就应该有针对性地减小正则化系数。

模型选择准则

模型选择的信息准则有很多,我这里介绍我知道的两个比较常用的模型选择准则:

  1. AIC准则

    赤池信息准则(Akaike Information Criterion,AIC)公式定义如下:其中k表示模型参数个数(复杂度),L表示经验误差(似然函数)。
    当需要从一组可供选择的模型中选择最佳模型时,通常选择AIC最小的模型。
  2. BIC准则

    贝叶斯信息准则(Bayesian Information Criterion,BIC)是对AIC准则的改进,定义如下:与AIC不同,这里k的系数不再是常数。其中n代表的是样本量(数据量),这样,BIC准则就与样本量相关了。当样本量足够时,过拟合的风险变小,我们就可以允许模型复杂一些。
    这里再次附上这张直观的图片,方便理解与体会。简析可参考machine-learning笔记:机器学习中正则化的理解

碰到底线咯 后面没有啦

本文标题:machine learning笔记:过拟合与欠拟合

文章作者:高深远

发布时间:2019年10月01日 - 10:45

最后更新:2020年01月30日 - 12:22

原始链接:https://gsy00517.github.io/machine-learning20191001104538/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:机器学习的几个常见算法及其优缺点 | 高深远的博客

machine learning笔记:机器学习的几个常见算法及其优缺点

接触机器学习也有一段较长的时间了,不敢说自己全部掌握甚至精通,但是期间也了解或者尝试了许多机器学习的算法。这次就结合参考资料和我自己的感受小结一下几种机器学习的常见算法及其优点和缺点。


决策树算法

学过数据结构中的树应该对这个算法不会感到困惑,下面就简单介绍一下其优缺点。

  1. 优点

    • 易于理解和解释,可以可视化分析,容易提取出规则。
    • 可以同时处理标称型和数值型数据。
    • 测试数据集时,运行速度比较快。
    • 决策树可以很好的扩展到大型数据库中,同时它的大小独立于数据库大小。
  2. 缺点

    • 对缺失数据处理比较困难。
    • 容易出现过拟合问题,容易受到例外的干扰,对测试集非常不友好。
    • 忽略数据集中属性的相互关联。
    • ID3算法计算信息增益时结果偏向数值比较多的特征。
  3. 改进措施

    • 对决策树进行剪枝。可以采用交叉验证法和加入正则化的方法。较为理想的决策树是叶子节点数少且深度较小。
    • 使用基于决策树的combination算法,如bagging算法,randomforest算法,可以解决过拟合的问题。
  4. 常见算法

    1. C4.5算法

      ID3算法是以信息论为基础,以信息熵和信息增益度为衡量标准,从而实现对数据的归纳分类。ID3算法计算每个属性的信息增益,并选取具有最高增益的属性作为给定的测试属性。C4.5算法核心思想是ID3算法,是ID3算法的改进,改进方面有:
      • 用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足。
      • 在树构造过程中进行剪枝。
      • 能处理非离散的数据。
      • 能处理不完整的数据。

        优点

      • 产生的分类规则易于理解,准确率较高。

        缺点

      • 在构造树的过程中,需要对数据集进行多次的顺序扫描和排序,因而导致算法的低效。
      • C4.5只适合于能够驻留于内存的数据集,当训练集大得无法在内存容纳时程序无法运行。
    2. CART分类与回归树

      这是一种决策树分类方法,采用基于最小距离的基尼指数估计函数,用来决定由该子数据集生成的决策树的拓展形。如果目标变量是标称的,称为分类树;如果目标变量是连续的,称为回归树。分类树是使用树结构算法将数据分成离散类的方法。

      优点

      • 非常灵活,可以允许有部分错分成本,还可指定先验概率分布,可使用自动的成本复杂性剪枝来得到归纳性更强的树。
      • 在面对诸如存在缺失值、变量数多等问题时CART显得非常稳健。

      下面对决策树的各种算法做一个小结:
      算法支持模型树结构特征选择
      ID3分类多叉树信息增益
      C4.5分类多叉树信息增益比
      CART分类、回归二叉树基尼系数、均方差

      补充:
      信息熵:表示随机变量的不确定性,熵越大,不确定性越大。这与物理中的熵性质类似。
      信息增益:即不确定性减小的幅度。信息增益=信息熵(前)-信息熵(后)。在构造决策树的时候往往选择信息增益大的特征优先作为节点分类标准。
      信息增益比:由于仅根据信息增益构建决策树,那么三叉树以及多叉树比二叉树的效果一般来说分类效果要好,然而这很有可能会导致过拟合的问题。因此定义信息增益比=惩罚参数*信息增益。当特征个数较多时,惩罚参数较小;当特征个数较少时,惩罚参数较大,从而使信息增益比较大,进而克服信息增益偏向于选取取值较多的特征的问题。总的来说,信息增益比相对于信息增益更客观。
      基尼系数:表示集合的不确定性,基尼系数越大,则表示不平等程度越高。


分类算法

  1. KNN算法

    1. 优点

      • KNN是一种在线技术,新数据可以直接加入数据集而不必进行重新训练。
      • KNN理论简单,容易实现。实际上,KNN没有训练过程,或者说,它的训练过程就是导入数据集。
    2. 缺点

      • KNN对于样本容量大的数据集计算量比较大,极易引发维度灾难。
      • 样本不平衡时,预测偏差比较大。如:某一类的样本比较少,而其它类样本比较多。
      • KNN每一次分类都会重新进行一次全局运算,耗时久。这在实践中会非常有体会,可以参考kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集
      • 在CV领域,KNN已经被完全弃用。这是因为它不适合用来表示图像之间的视觉感知差异,如下图所示,这是CS231n中提到的一个例子,后三张图片经过不同的变换,结果与第一张原图的L2距离居然是一样的,而显然对我们而言这三张图是有很大区别的,在实际应用中往往应该区分开。
    3. 应用领域

      • 文本分类。
      • 模式识别。
      • 聚类分析。
      • 多分类领域。
  2. 支持向量机(SVM)

    支持向量机是一种基于分类边界的方法。其基本原理是(以二维数据为例):如果训练数据分布在二维平面上的点,它们按照其分类聚集在不同的区域。基于分类边界的分类算法的目标是:通过训练,找到这些分类之间的边界(直线的称为线性划分,曲线的称为非线性划分)。对于多维数据(如N维),可以将它们视为N维空间中的点,而分类边界就是N维空间中的面,称为超面(超面比N维空间少一维)。线性分类器使用超平面类型的边界,非线性分类器使用超曲面。
    支持向量机的原理是将低维空间的点映射到高维空间,使它们成为线性可分,再使用线性划分的原理来判断分类边界。在高维空间中是一种线性划分,而在原有的数据空间中,是一种非线性划分。
    在我的博文machine-learning笔记:一个支持向量机的问题中,我提及了SVM的简介与一个问题,感兴趣的话可以了解一下。
    1. 优点

      • 解决小样本下机器学习问题,相对于其他训练分类算法不需要过多样本。
      • 解决非线性问题。擅长应付线性不可分,主要用松弛变量(惩罚变量)和核函数来实现。
      • 无局部极小值问题。(相对于神经网络等算法)
      • 引入了核函数,可以很好的处理高维数据集。
      • 泛化能力比较强。结构风险最小,指分类器对问题真实模型的逼近与真实解之间的累计误差。
    2. 缺点

      • 对于核函数的高维映射解释力不强,尤其是径向基函数。
      • 对缺失数据敏感。
    3. 应用领域:

      • 文本分类。
      • 图像识别。
      • 主要二分类领域。
  3. 朴素贝叶斯算法

    朴素贝叶斯,即naive bayes。说白了就是要“sometimes naive”。
    1. 优点

      • 对大数量训练和查询时具有较高的速度。即使使用超大规模的训练集,针对每个项目通常也只会有相对较少的特征数,并且对项目的训练和分类也仅仅是特征概率的数学运算而已。
      • 支持增量式运算。即可以实时的对新增的样本进行训练。
      • 朴素贝叶斯对结果解释容易理解。
    2. 缺点

      • 由于使用了样本属性独立性的假设,所以如果样本属性有关联时其效果不好。
    3. 应用领域

      • 文本分类。
      • 欺诈检测。
  4. Logistic回归算法

    1. 优点

      • 计算代价不高,易于理解和实现。
    2. 缺点

      • 容易产生欠拟合。
      • 分类精度不高。
    3. 应用领域

      • 用于二分类领域,可以得出概率值,适用于根据分类概率排名的领域,如搜索排名等。
      • Logistic回归的扩展softmax可以应用于多分类领域,如手写字识别等。

聚类算法

  1. K-means算法

    K-means算法,即K均值算法,是一个简单的聚类算法,把n个对象根据它们的属性分为k个分割,k小于n。算法的核心就是要优化失真函数J,使其收敛到局部最小值但不是全局最小值。它比较适合凸数据集,即任意两个数据点之间的连线都在数据集内部。
    1. 算法流程

      1. 随机选择k个随机的点(称为聚类中心)。
      2. 对数据集中的每个数据点,按照距离k个中心的距离,将其与最近的中心点关联起来,与同一中心点关联的点聚成一类。
      3. 计算每一组的均值,将该组所关联的中心点移到平均值的位置。
      4. 重复第2、3两步,直到中心点不再变化。
    2. 优点

      • 算法速度很快。
    3. 缺点

      • 分组的数目k是一个输入超参数,不合适的k可能返回较差的结果。
  2. EM最大期望算法

    EM算法是基于模型的聚类方法,是在概率模型中寻找参数最大似然估计的算法,其中概率模型依赖于无法观测的隐藏变量。E步估计隐含变量,M步估计其他参数,交替将极值推向最大。
    EM算法比K-means算法计算复杂,收敛也较慢,不适于大规模数据集和高维数据,但比K-means算法计算结果稳定、准确。EM经常用在机器学习和计算机视觉的数据集聚(Data Clustering)领域。

集成算法(AdaBoost)

俗话说的好“三个臭皮匠,顶个诸葛亮”,集成算法就是将多个弱分类器集成在一起,构建一个强分类器。事实上,它可能不属于算法,而更像一种优化手段。

  1. 优点

    • 很好的利用了弱分类器进行级联。
    • 可以将不同的分类算法作为弱分类器。
    • AdaBoost具有很高的精度。
    • 相对于bagging算法和randomforest算法,AdaBoost充分考虑的每个分类器的权重。
  2. 缺点

    • AdaBoost迭代次数也就是弱分类器数目不太好设定,可以使用交叉验证来进行确定。
    • 数据不平衡导致分类精度下降。
    • 训练比较耗时,每次重新选择当前分类器最好切分点。
  3. 应用领域

    • 模式识别。
    • 计算机视觉领域。
    • 二分类和多分类场景。

神经网络算法

  1. 优点

    • 分类准确度高,学习能力极强。
    • 对噪声数据鲁棒性和容错性较强。
    • 有联想能力,能逼近任意非线性关系。
  2. 缺点

  3. 应用领域

    • 计算机视觉。
    • 自然语言处理。
    • 语音识别等。

碰到底线咯 后面没有啦

本文标题:machine learning笔记:机器学习的几个常见算法及其优缺点

文章作者:高深远

发布时间:2019年11月01日 - 19:20

最后更新:2020年01月19日 - 15:36

原始链接:https://gsy00517.github.io/machine-learning20191101192042/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:机器学习的几个常见算法及其优缺点 | 高深远的博客

machine learning笔记:机器学习的几个常见算法及其优缺点

接触机器学习也有一段较长的时间了,不敢说自己全部掌握甚至精通,但是期间也了解或者尝试了许多机器学习的算法。这次就结合参考资料和我自己的感受小结一下几种机器学习的常见算法及其优点和缺点。


决策树算法

学过数据结构中的树应该对这个算法不会感到困惑,下面就简单介绍一下其优缺点。

  1. 优点

    • 易于理解和解释,可以可视化分析,容易提取出规则。
    • 可以同时处理标称型和数值型数据。
    • 测试数据集时,运行速度比较快。
    • 决策树可以很好的扩展到大型数据库中,同时它的大小独立于数据库大小。
  2. 缺点

    • 对缺失数据处理比较困难。
    • 容易出现过拟合问题,容易受到例外的干扰,对测试集非常不友好。
    • 忽略数据集中属性的相互关联。
    • ID3算法计算信息增益时结果偏向数值比较多的特征。
  3. 改进措施

    • 对决策树进行剪枝。可以采用交叉验证法和加入正则化的方法。较为理想的决策树是叶子节点数少且深度较小。
    • 使用基于决策树的combination算法,如bagging算法,randomforest算法,可以解决过拟合的问题。
  4. 常见算法

    1. C4.5算法

      ID3算法是以信息论为基础,以信息熵和信息增益度为衡量标准,从而实现对数据的归纳分类。ID3算法计算每个属性的信息增益,并选取具有最高增益的属性作为给定的测试属性。C4.5算法核心思想是ID3算法,是ID3算法的改进,改进方面有:
      • 用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足。
      • 在树构造过程中进行剪枝。
      • 能处理非离散的数据。
      • 能处理不完整的数据。

        优点

      • 产生的分类规则易于理解,准确率较高。

        缺点

      • 在构造树的过程中,需要对数据集进行多次的顺序扫描和排序,因而导致算法的低效。
      • C4.5只适合于能够驻留于内存的数据集,当训练集大得无法在内存容纳时程序无法运行。
    2. CART分类与回归树

      这是一种决策树分类方法,采用基于最小距离的基尼指数估计函数,用来决定由该子数据集生成的决策树的拓展形。如果目标变量是标称的,称为分类树;如果目标变量是连续的,称为回归树。分类树是使用树结构算法将数据分成离散类的方法。

      优点

      • 非常灵活,可以允许有部分错分成本,还可指定先验概率分布,可使用自动的成本复杂性剪枝来得到归纳性更强的树。
      • 在面对诸如存在缺失值、变量数多等问题时CART显得非常稳健。

      下面对决策树的各种算法做一个小结:
      算法支持模型树结构特征选择
      ID3分类多叉树信息增益
      C4.5分类多叉树信息增益比
      CART分类、回归二叉树基尼系数、均方差

      补充:
      信息熵:表示随机变量的不确定性,熵越大,不确定性越大。这与物理中的熵性质类似。
      信息增益:即不确定性减小的幅度。信息增益=信息熵(前)-信息熵(后)。在构造决策树的时候往往选择信息增益大的特征优先作为节点分类标准。
      信息增益比:由于仅根据信息增益构建决策树,那么三叉树以及多叉树比二叉树的效果一般来说分类效果要好,然而这很有可能会导致过拟合的问题。因此定义信息增益比=惩罚参数*信息增益。当特征个数较多时,惩罚参数较小;当特征个数较少时,惩罚参数较大,从而使信息增益比较大,进而克服信息增益偏向于选取取值较多的特征的问题。总的来说,信息增益比相对于信息增益更客观。
      基尼系数:表示集合的不确定性,基尼系数越大,则表示不平等程度越高。


分类算法

  1. KNN算法

    1. 优点

      • KNN是一种在线技术,新数据可以直接加入数据集而不必进行重新训练。
      • KNN理论简单,容易实现。实际上,KNN没有训练过程,或者说,它的训练过程就是导入数据集。
    2. 缺点

      • KNN对于样本容量大的数据集计算量比较大,极易引发维度灾难。
      • 样本不平衡时,预测偏差比较大。如:某一类的样本比较少,而其它类样本比较多。
      • KNN每一次分类都会重新进行一次全局运算,耗时久。这在实践中会非常有体会,可以参考kaggle笔记:手写数字识别——使用KNN和CNN尝试MNIST数据集
      • 在CV领域,KNN已经被完全弃用。这是因为它不适合用来表示图像之间的视觉感知差异,如下图所示,这是CS231n中提到的一个例子,后三张图片经过不同的变换,结果与第一张原图的L2距离居然是一样的,而显然对我们而言这三张图是有很大区别的,在实际应用中往往应该区分开。
    3. 应用领域

      • 文本分类。
      • 模式识别。
      • 聚类分析。
      • 多分类领域。
  2. 支持向量机(SVM)

    支持向量机是一种基于分类边界的方法。其基本原理是(以二维数据为例):如果训练数据分布在二维平面上的点,它们按照其分类聚集在不同的区域。基于分类边界的分类算法的目标是:通过训练,找到这些分类之间的边界(直线的称为线性划分,曲线的称为非线性划分)。对于多维数据(如N维),可以将它们视为N维空间中的点,而分类边界就是N维空间中的面,称为超面(超面比N维空间少一维)。线性分类器使用超平面类型的边界,非线性分类器使用超曲面。
    支持向量机的原理是将低维空间的点映射到高维空间,使它们成为线性可分,再使用线性划分的原理来判断分类边界。在高维空间中是一种线性划分,而在原有的数据空间中,是一种非线性划分。
    在我的博文machine-learning笔记:一个支持向量机的问题中,我提及了SVM的简介与一个问题,感兴趣的话可以了解一下。
    1. 优点

      • 解决小样本下机器学习问题,相对于其他训练分类算法不需要过多样本。
      • 解决非线性问题。擅长应付线性不可分,主要用松弛变量(惩罚变量)和核函数来实现。
      • 无局部极小值问题。(相对于神经网络等算法)
      • 引入了核函数,可以很好的处理高维数据集。
      • 泛化能力比较强。结构风险最小,指分类器对问题真实模型的逼近与真实解之间的累计误差。
    2. 缺点

      • 对于核函数的高维映射解释力不强,尤其是径向基函数。
      • 对缺失数据敏感。
    3. 应用领域:

      • 文本分类。
      • 图像识别。
      • 主要二分类领域。
  3. 朴素贝叶斯算法

    朴素贝叶斯,即naive bayes。说白了就是要“sometimes naive”。
    1. 优点

      • 对大数量训练和查询时具有较高的速度。即使使用超大规模的训练集,针对每个项目通常也只会有相对较少的特征数,并且对项目的训练和分类也仅仅是特征概率的数学运算而已。
      • 支持增量式运算。即可以实时的对新增的样本进行训练。
      • 朴素贝叶斯对结果解释容易理解。
    2. 缺点

      • 由于使用了样本属性独立性的假设,所以如果样本属性有关联时其效果不好。
    3. 应用领域

      • 文本分类。
      • 欺诈检测。
  4. Logistic回归算法

    1. 优点

      • 计算代价不高,易于理解和实现。
    2. 缺点

      • 容易产生欠拟合。
      • 分类精度不高。
    3. 应用领域

      • 用于二分类领域,可以得出概率值,适用于根据分类概率排名的领域,如搜索排名等。
      • Logistic回归的扩展softmax可以应用于多分类领域,如手写字识别等。

聚类算法

  1. K-means算法

    K-means算法,即K均值算法,是一个简单的聚类算法,把n个对象根据它们的属性分为k个分割,k小于n。算法的核心就是要优化失真函数J,使其收敛到局部最小值但不是全局最小值。它比较适合凸数据集,即任意两个数据点之间的连线都在数据集内部。
    1. 算法流程

      1. 随机选择k个随机的点(称为聚类中心)。
      2. 对数据集中的每个数据点,按照距离k个中心的距离,将其与最近的中心点关联起来,与同一中心点关联的点聚成一类。
      3. 计算每一组的均值,将该组所关联的中心点移到平均值的位置。
      4. 重复第2、3两步,直到中心点不再变化。
    2. 优点

      • 算法速度很快。
    3. 缺点

      • 分组的数目k是一个输入超参数,不合适的k可能返回较差的结果。
  2. EM最大期望算法

    EM算法是基于模型的聚类方法,是在概率模型中寻找参数最大似然估计的算法,其中概率模型依赖于无法观测的隐藏变量。E步估计隐含变量,M步估计其他参数,交替将极值推向最大。
    EM算法比K-means算法计算复杂,收敛也较慢,不适于大规模数据集和高维数据,但比K-means算法计算结果稳定、准确。EM经常用在机器学习和计算机视觉的数据集聚(Data Clustering)领域。

集成算法(AdaBoost)

俗话说的好“三个臭皮匠,顶个诸葛亮”,集成算法就是将多个弱分类器集成在一起,构建一个强分类器。事实上,它可能不属于算法,而更像一种优化手段。

  1. 优点

    • 很好的利用了弱分类器进行级联。
    • 可以将不同的分类算法作为弱分类器。
    • AdaBoost具有很高的精度。
    • 相对于bagging算法和randomforest算法,AdaBoost充分考虑的每个分类器的权重。
  2. 缺点

    • AdaBoost迭代次数也就是弱分类器数目不太好设定,可以使用交叉验证来进行确定。
    • 数据不平衡导致分类精度下降。
    • 训练比较耗时,每次重新选择当前分类器最好切分点。
  3. 应用领域

    • 模式识别。
    • 计算机视觉领域。
    • 二分类和多分类场景。

神经网络算法

  1. 优点

    • 分类准确度高,学习能力极强。
    • 对噪声数据鲁棒性和容错性较强。
    • 有联想能力,能逼近任意非线性关系。
  2. 缺点

  3. 应用领域

    • 计算机视觉。
    • 自然语言处理。
    • 语音识别等。

碰到底线咯 后面没有啦

本文标题:machine learning笔记:机器学习的几个常见算法及其优缺点

文章作者:高深远

发布时间:2019年11月01日 - 19:20

最后更新:2020年01月19日 - 15:36

原始链接:https://gsy00517.github.io/machine-learning20191101192042/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:数据采样之正样本和负样本 | 高深远的博客

machine learning笔记:数据采样之正样本和负样本

今天在看最小二乘回归(least squares regression)时看到作者把positive examples设成1,把negative examples设成0。感觉对这个概念既熟悉又陌生,查了一下之后一下子想起来了。在机器学习中,数据预处理一般包括数据清洗、数据集成、数据采样。而正负样本涉及到了数据采样的问题,因此后面也提一下。


正样本和负样本

简单来说,和概率论中类似,一般我们看一个问题时,只关注一个事件(希望它发生或者成功,并对其进行分析计算),而正样本就是属于我们关注的这一类别的样本,负样本就是指不属于该类别的样本。


数据采样平衡

一般来说,比如我们训练分类器时,希望样本中正负样本的比例是接近于1:1的。因为如果正样本占比很大(比如90%)或者负样本占比远超正样本,那么训练结果可想而知,获得的分类器在测试中的效果会很差。
针对这种数据不平衡的问题,有以下三种solution:

  1. 过采样(over-sampling)

    这是一种较为直接的办法,即通过随机复制少数类来增加其中的实例数量,从而可增加样本中少数类的代表性。
  2. 欠采样(under-sampling)

    这种方法也比较直接,即通过随机消除占多数类的样本来平衡类分布,直到多数类和少数类实现平衡。
  3. 获取更多样本

    上面的两种方法比较直接方便,但也存在弊端,比如过采样可能会导致过拟合,欠采样可能无法很好地利用有限的数据(这也可能会造成过拟合)。因此最好还是获取更多的样本来补充,我认为主要有下面两种方法:
    1. 采集

      例如在海贼王漫画的样本中,我们要进行20x20大小的海贼检测,那么为了获取尽可能多的负样本,我们可以截取一张1000x1000大小的海王类图像,将其拆分为20x20大小的片段加入到负样本中(即50x50地进行分割)。
    2. 生成

      为了获得更多负样本,我们也可将前面1000x1000的海王类图像先拆分为10x10大小,这就比之前多出了4倍的负样本图像。不过要注意的是,为了保持大小的一致,还需进一步将其拉伸至20x20的大小。 当然,其实不需要从体积上达到这么大的比例,关键是像素尺寸的匹配。

碰到底线咯 后面没有啦

本文标题:machine learning笔记:数据采样之正样本和负样本

文章作者:高深远

发布时间:2020年01月18日 - 11:21

最后更新:2020年01月20日 - 13:28

原始链接:https://gsy00517.github.io/machine-learning20200118112156/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:数据采样之正样本和负样本 | 高深远的博客

machine learning笔记:数据采样之正样本和负样本

今天在看最小二乘回归(least squares regression)时看到作者把positive examples设成1,把negative examples设成0。感觉对这个概念既熟悉又陌生,查了一下之后一下子想起来了。在机器学习中,数据预处理一般包括数据清洗、数据集成、数据采样。而正负样本涉及到了数据采样的问题,因此后面也提一下。


正样本和负样本

简单来说,和概率论中类似,一般我们看一个问题时,只关注一个事件(希望它发生或者成功,并对其进行分析计算),而正样本就是属于我们关注的这一类别的样本,负样本就是指不属于该类别的样本。


数据采样平衡

一般来说,比如我们训练分类器时,希望样本中正负样本的比例是接近于1:1的。因为如果正样本占比很大(比如90%)或者负样本占比远超正样本,那么训练结果可想而知,获得的分类器在测试中的效果会很差。
针对这种数据不平衡的问题,有以下三种solution:

  1. 过采样(over-sampling)

    这是一种较为直接的办法,即通过随机复制少数类来增加其中的实例数量,从而可增加样本中少数类的代表性。
  2. 欠采样(under-sampling)

    这种方法也比较直接,即通过随机消除占多数类的样本来平衡类分布,直到多数类和少数类实现平衡。
  3. 获取更多样本

    上面的两种方法比较直接方便,但也存在弊端,比如过采样可能会导致过拟合,欠采样可能无法很好地利用有限的数据(这也可能会造成过拟合)。因此最好还是获取更多的样本来补充,我认为主要有下面两种方法:
    1. 采集

      例如在海贼王漫画的样本中,我们要进行20x20大小的海贼检测,那么为了获取尽可能多的负样本,我们可以截取一张1000x1000大小的海王类图像,将其拆分为20x20大小的片段加入到负样本中(即50x50地进行分割)。
    2. 生成

      为了获得更多负样本,我们也可将前面1000x1000的海王类图像先拆分为10x10大小,这就比之前多出了4倍的负样本图像。不过要注意的是,为了保持大小的一致,还需进一步将其拉伸至20x20的大小。 当然,其实不需要从体积上达到这么大的比例,关键是像素尺寸的匹配。

碰到底线咯 后面没有啦

本文标题:machine learning笔记:数据采样之正样本和负样本

文章作者:高深远

发布时间:2020年01月18日 - 11:21

最后更新:2020年01月20日 - 13:28

原始链接:https://gsy00517.github.io/machine-learning20200118112156/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:闭式解 | 高深远的博客

machine learning笔记:闭式解

看论文总是会看到许多新奇的名词,比如“one-shot learning”(单样本学习)即仅从一个或者很少的样本中学习训练。还有诸如“closed-form”、“closed-form solution”,看的我真是很纳闷,这里就结合资料好好理理清楚。

References

电子文献:
https://blog.csdn.net/yy16808/article/details/76493384
https://blog.csdn.net/langjueyun2010/article/details/80348449


解析解与数值解

这里先介绍两个相对应的数学概念:解析解与数值解。
直接上例子:解$x^{2}=3$。
那么这题的解析解是:$x=\sqrt{3}$。
数值解为:$x=1.732$。
简而言之,解析解就是给出解的具体函数形式,从解的表达式中就可以算出任何对应值,好像就是小学所谓的公式解;而数值解就是直接用数值方法求出具体的解。


闭式解

实际上,闭式解也被称为解析解。由于解析解为一封闭形式(closed-form)的函数,因此对任一独立变量,我们皆可将其带入解析函数求得正确的相依变量。即解可以表达为一个函数形式,带入变量即可得到解。
一般而言,有闭式解的优化方法效率更高。
这就写完了,似乎很简单。


碰到底线咯 后面没有啦

本文标题:machine learning笔记:闭式解

文章作者:高深远

发布时间:2020年01月19日 - 14:56

最后更新:2020年02月19日 - 10:06

原始链接:https://gsy00517.github.io/machine-learning20200119145637/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:闭式解 | 高深远的博客

machine learning笔记:闭式解

看论文总是会看到许多新奇的名词,比如“one-shot learning”(单样本学习)即仅从一个或者很少的样本中学习训练。还有诸如“closed-form”、“closed-form solution”,看的我真是很纳闷,这里就结合资料好好理理清楚。

References

电子文献:
https://blog.csdn.net/yy16808/article/details/76493384
https://blog.csdn.net/langjueyun2010/article/details/80348449


解析解与数值解

这里先介绍两个相对应的数学概念:解析解与数值解。
直接上例子:解$x^{2}=3$。
那么这题的解析解是:$x=\sqrt{3}$。
数值解为:$x=1.732$。
简而言之,解析解就是给出解的具体函数形式,从解的表达式中就可以算出任何对应值,好像就是小学所谓的公式解;而数值解就是直接用数值方法求出具体的解。


闭式解

实际上,闭式解也被称为解析解。由于解析解为一封闭形式(closed-form)的函数,因此对任一独立变量,我们皆可将其带入解析函数求得正确的相依变量。即解可以表达为一个函数形式,带入变量即可得到解。
一般而言,有闭式解的优化方法效率更高。
这就写完了,似乎很简单。


碰到底线咯 后面没有啦

本文标题:machine learning笔记:闭式解

文章作者:高深远

发布时间:2020年01月19日 - 14:56

最后更新:2020年02月19日 - 10:06

原始链接:https://gsy00517.github.io/machine-learning20200119145637/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:准确率和召回率 | 高深远的博客

machine learning笔记:准确率和召回率

在二分类问题中,我们常用准确率(precision)和召回率(recall)来进行评价。实际上,许多问题都可以转变为二分类问题。对于这其中的一些概念,之前都是死记硬背,今天在看目标检测的评价指标时突然有一点理解,赶快写下来。

References

参考文献:
[1]统计学习方法(第2版)


4种情况

通常我们把关注的类称为正类,其它类称为负类。而分类器在测试数据集上的预测可能是正确的也可能是不正确的。由此就出现了四种情况。
TP:True Positive(真的正类),即把正类预测为正类的个数。
FN:False Negative(假的负类),即把正类预测为负类的个数。
FP:False Positive,即把负类预测为正类的个数。
TN:True Negative,即把负类预测为负类的个数。一般不关注这一项。


准确率和召回率

由此我们可定义准确率为$P=\frac{TP}{TP+FP}$。
定义召回率为$R=\frac{TP}{TP+FN}$。
此外,还定义了$F_{1}$为准确率和召回率的调和均值,即$\frac{2}{F_{1}}=\frac{1}{P}+\frac{1}{R}$。可变换定义式求得$F_{1}$值。易得,当准确率和召回率都高时,$F_{1}$也会高。
我们可以结合下图来直观地理解一下。

结合上图和准确率与召回率地定义,由于一般在问题中我们只对我们预测的正类做标注,因此我觉得可以这样理解准确率和召回率:
准确率:预测出的结果中,有多少是正确的。
召回率:所有属于正类的目标中,有多少被正确地预测出来了。


碰到底线咯 后面没有啦

本文标题:machine learning笔记:准确率和召回率

文章作者:高深远

发布时间:2020年01月30日 - 12:18

最后更新:2020年01月30日 - 14:00

原始链接:https://gsy00517.github.io/machine-learning20200130121842/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:准确率和召回率 | 高深远的博客

machine learning笔记:准确率和召回率

在二分类问题中,我们常用准确率(precision)和召回率(recall)来进行评价。实际上,许多问题都可以转变为二分类问题。对于这其中的一些概念,之前都是死记硬背,今天在看目标检测的评价指标时突然有一点理解,赶快写下来。

References

参考文献:
[1]统计学习方法(第2版)


4种情况

通常我们把关注的类称为正类,其它类称为负类。而分类器在测试数据集上的预测可能是正确的也可能是不正确的。由此就出现了四种情况。
TP:True Positive(真的正类),即把正类预测为正类的个数。
FN:False Negative(假的负类),即把正类预测为负类的个数。
FP:False Positive,即把负类预测为正类的个数。
TN:True Negative,即把负类预测为负类的个数。一般不关注这一项。


准确率和召回率

由此我们可定义准确率为$P=\frac{TP}{TP+FP}$。
定义召回率为$R=\frac{TP}{TP+FN}$。
此外,还定义了$F_{1}$为准确率和召回率的调和均值,即$\frac{2}{F_{1}}=\frac{1}{P}+\frac{1}{R}$。可变换定义式求得$F_{1}$值。易得,当准确率和召回率都高时,$F_{1}$也会高。
我们可以结合下图来直观地理解一下。

结合上图和准确率与召回率地定义,由于一般在问题中我们只对我们预测的正类做标注,因此我觉得可以这样理解准确率和召回率:
准确率:预测出的结果中,有多少是正确的。
召回率:所有属于正类的目标中,有多少被正确地预测出来了。


碰到底线咯 后面没有啦

本文标题:machine learning笔记:准确率和召回率

文章作者:高深远

发布时间:2020年01月30日 - 12:18

最后更新:2020年01月30日 - 14:00

原始链接:https://gsy00517.github.io/machine-learning20200130121842/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:SVD与字典学习 | 高深远的博客

machine learning笔记:SVD与字典学习

SVD,译为奇异值分解,是一种常用的数据降维方式。一般我们可以对实兑成矩阵作特征值分解,而SVD就类似于对一般情况下MxN的实数矩阵作“特征值分解”,称结果中对角线上的值为奇异值。
而字典学习(Dictionary Learning),又叫KSVD,是一种常用的稀疏表示方法,其本质就是经过K轮迭代,而每次迭代都使用SVD来降维。


分享

SVD
字典学习
关于SVD和字典学习,这位博主的这两篇文章写得很棒,思路清晰准确,尤其是排版让人赏心悦目。自己功力不足,又怕放在收藏夹里吃灰,附链接在此方便日后学习。


代码实现

这里以scipy库中提供的样本图片为例(原本有计算机视觉女生Lena,现换成了一张爬楼梯图),对字典学习的实现过程做一个比较简单的展示和比较详细的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#基本准备
import numpy as np #待会直接调用numpy.linalg.svd函数来实现SVD
from sklearn import linear_model
import scipy.misc #这是一个图像处理的库,待会需借用其中的图片样本
import matplotlib.pyplot as plt #画图要用

#稀疏模型Y = DX,Y为样本矩阵,使用KSVD动态更新字典矩阵D和稀疏矩阵X

class KSVD(object): #单继承object()的属性
def __init__(self, n_components, max_iter = 30, tol = 1e-6,
n_nonzero_coefs = None):
self.dictionary = None
self.sparsecode = None
self.max_iter = max_iter #最大迭代次数
self.tol = tol #稀疏表示结果的容差
self.n_components = n_components #字典所含原子个数(字典的列数)
self.n_nonzero_coefs = n_nonzero_coefs #稀疏度

#初始化字典矩阵
def _initialize(self, y):
u, s, v = np.linalg.svd(y)
self.dictionary = u[:, :self.n_components]

#使用KSVD更新字典的过程
def _update_dict(self, y, d, x):
for i in range(self.n_components):
#选择X中第i行(从0开始)中非零项
index = np.nonzero(x[i, :])[0]
#如果没有非零项,则直接进入下一次for循环
if len(index) == 0:
continue

#更新D中第i列
d[:, i] = 0
#计算误差矩阵
r = (y - np.dot(d, x))[:, index]
#利用SVD的方法,来求解更新字典和稀疏系数矩阵
u, s, v = np.linalg.svd(r, full_matrices = False)
#使用左奇异矩阵的第0列更新字典
d[:, i] = u[:, 0].T
#使用第0个奇异值和右奇异矩阵的第0行的乘积更新稀疏系数矩阵
x[i, index] = s[0] * v[0, :]
return d, x

#KSVD迭代过程
def fit(self, y):
self._initialize(y)
for i in range(self.max_iter): #在最大迭代范围内
#稀疏编码
x = linear_model.orthogonal_mp(self.dictionary, y, n_nonzero_coefs = self.n_nonzero_coefs)
#计算容差
e = np.linalg.norm(y - np.dot(self.dictionary, x))
#满足容差就结束
if e < self.tol:
break
#更新字典
self._update_dict(y, self.dictionary, x)

#稀疏编码
self.sparsecode = linear_model.orthogonal_mp(self.dictionary, y, n_nonzero_coefs = self.n_nonzero_coefs)

return self.dictionary, self.sparsecode


if __name__ == '__main__': #作为脚本时直接执行,但被import至其它脚本时不会被执行
#调用scipy中的爬楼梯图
im_ascent = scipy.misc.ascent().astype(np.float)
ksvd = KSVD(100) #100列的字典
dictionary, sparsecode = ksvd.fit(im_ascent)

plt.figure()
#创建子图,1行2列中的第1张图片
plt.subplot(1, 2, 1)
plt.imshow(im_ascent) #原图
#创建子图,1行2列中的第2张图片
plt.subplot(1, 2, 2)
plt.imshow(dictionary.dot(sparsecode))
#dictionary.dot(sparsecode)等价于numpy.dot(dictionary, sparsecode),即矩阵相乘,这里是DX = Y
plt.show()

以下是字典中“词汇量”(即可表示属性的个数)不同时的三种结果。



碰到底线咯 后面没有啦

本文标题:machine learning笔记:SVD与字典学习

文章作者:高深远

发布时间:2020年02月12日 - 22:16

最后更新:2020年02月14日 - 22:50

原始链接:https://gsy00517.github.io/machine-learning20200212221654/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
machine learning笔记:SVD与字典学习 | 高深远的博客

machine learning笔记:SVD与字典学习

SVD,译为奇异值分解,是一种常用的数据降维方式。一般我们可以对实兑成矩阵作特征值分解,而SVD就类似于对一般情况下MxN的实数矩阵作“特征值分解”,称结果中对角线上的值为奇异值。
而字典学习(Dictionary Learning),又叫KSVD,是一种常用的稀疏表示方法,其本质就是经过K轮迭代,而每次迭代都使用SVD来降维。


分享

SVD
字典学习
关于SVD和字典学习,这位博主的这两篇文章写得很棒,思路清晰准确,尤其是排版让人赏心悦目。自己功力不足,又怕放在收藏夹里吃灰,附链接在此方便日后学习。


代码实现

这里以scipy库中提供的样本图片为例(原本有计算机视觉女生Lena,现换成了一张爬楼梯图),对字典学习的实现过程做一个比较简单的展示和比较详细的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#基本准备
import numpy as np #待会直接调用numpy.linalg.svd函数来实现SVD
from sklearn import linear_model
import scipy.misc #这是一个图像处理的库,待会需借用其中的图片样本
import matplotlib.pyplot as plt #画图要用

#稀疏模型Y = DX,Y为样本矩阵,使用KSVD动态更新字典矩阵D和稀疏矩阵X

class KSVD(object): #单继承object()的属性
def __init__(self, n_components, max_iter = 30, tol = 1e-6,
n_nonzero_coefs = None):
self.dictionary = None
self.sparsecode = None
self.max_iter = max_iter #最大迭代次数
self.tol = tol #稀疏表示结果的容差
self.n_components = n_components #字典所含原子个数(字典的列数)
self.n_nonzero_coefs = n_nonzero_coefs #稀疏度

#初始化字典矩阵
def _initialize(self, y):
u, s, v = np.linalg.svd(y)
self.dictionary = u[:, :self.n_components]

#使用KSVD更新字典的过程
def _update_dict(self, y, d, x):
for i in range(self.n_components):
#选择X中第i行(从0开始)中非零项
index = np.nonzero(x[i, :])[0]
#如果没有非零项,则直接进入下一次for循环
if len(index) == 0:
continue

#更新D中第i列
d[:, i] = 0
#计算误差矩阵
r = (y - np.dot(d, x))[:, index]
#利用SVD的方法,来求解更新字典和稀疏系数矩阵
u, s, v = np.linalg.svd(r, full_matrices = False)
#使用左奇异矩阵的第0列更新字典
d[:, i] = u[:, 0].T
#使用第0个奇异值和右奇异矩阵的第0行的乘积更新稀疏系数矩阵
x[i, index] = s[0] * v[0, :]
return d, x

#KSVD迭代过程
def fit(self, y):
self._initialize(y)
for i in range(self.max_iter): #在最大迭代范围内
#稀疏编码
x = linear_model.orthogonal_mp(self.dictionary, y, n_nonzero_coefs = self.n_nonzero_coefs)
#计算容差
e = np.linalg.norm(y - np.dot(self.dictionary, x))
#满足容差就结束
if e < self.tol:
break
#更新字典
self._update_dict(y, self.dictionary, x)

#稀疏编码
self.sparsecode = linear_model.orthogonal_mp(self.dictionary, y, n_nonzero_coefs = self.n_nonzero_coefs)

return self.dictionary, self.sparsecode


if __name__ == '__main__': #作为脚本时直接执行,但被import至其它脚本时不会被执行
#调用scipy中的爬楼梯图
im_ascent = scipy.misc.ascent().astype(np.float)
ksvd = KSVD(100) #100列的字典
dictionary, sparsecode = ksvd.fit(im_ascent)

plt.figure()
#创建子图,1行2列中的第1张图片
plt.subplot(1, 2, 1)
plt.imshow(im_ascent) #原图
#创建子图,1行2列中的第2张图片
plt.subplot(1, 2, 2)
plt.imshow(dictionary.dot(sparsecode))
#dictionary.dot(sparsecode)等价于numpy.dot(dictionary, sparsecode),即矩阵相乘,这里是DX = Y
plt.show()

以下是字典中“词汇量”(即可表示属性的个数)不同时的三种结果。



碰到底线咯 后面没有啦

本文标题:machine learning笔记:SVD与字典学习

文章作者:高深远

发布时间:2020年02月12日 - 22:16

最后更新:2020年02月14日 - 22:50

原始链接:https://gsy00517.github.io/machine-learning20200212221654/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
markdown笔记:markdown的基本使用 | 高深远的博客

markdown笔记:markdown的基本使用

既然要写技术博客,那么markdown肯定是必备的了,这篇文章就来介绍一下markdown的基本使用操作。

References

电子文献:
https://www.jianshu.com/p/191d1e21f7ed


介绍

Markdown是一种可以使用普通文本编辑器编写的标记语言,其功能比纯文本更强,因此许多程序员用它来写blog。在这里我先推荐一款markdown编辑器——typora,大家可以免费下载使用。


注意

在我刚开始使用markdown的时候总是跳进这个坑,在这里提上来提醒一下,在使用markdown标记后要添加文字时,需要在相应标记后空一格,否则标记也会被当作文本来处理,例如我输入“#####错误”时:

错误

正确的做法是输入“##### 正确”:

正确

一种简单的判别方法就是使用IDE,这样对应的标记就会有语法高亮。


使用

  1. 标题

    话不多说,直接示范:

    1
    2
    3
    4
    5
    6
    # 这是一级标题
    ## 这是二级标题
    ### 这是三级标题
    #### 这是四级标题
    ##### 这是五级标题
    ###### 这是六级标题

    效果如下:

    这是一级标题

    这是二级标题

    这是三级标题

    这是四级标题

    这是五级标题
    这是六级标题
  2. 字体

    还是直接示范:

    1
    2
    3
    4
    **这是加粗的文字**
    *这是倾斜的文字*`
    ***这是斜体加粗的文字***
    ~~这是加删除线的文字~~

    这是加粗的文字
    这是倾斜的文字
    这是斜体加粗的文字
    这是加删除线的文字

  3. 引用

    1
    2
    3
    4
    >我引用
    >>我还引用
    >>>我再引用
    >>>>>>>>>>>>>扶我起来,我还能继续引用!

    我引用

    我还引用

    我再引用

    扶我起来,我还能继续引用!

    引用是可以嵌套的,可以加很多层,我一般使用一个>来表示额外的需要注意的内容。另外,如果想让下一段文字不被引用,需要空一行。

  4. 分割线

    分割线使用三个及以上的-*就可以。
    有时候用---会造成别的文字的格式变化,因此我在使用VScode编辑时,如果看到---被高亮(分割线正常其作用时应该不高亮),就会改用***

    1
    2
    ---
    ***

    效果如下:



  5. 图片

    markdown中添加图片的语法是这样的:

    1
    ![显示在图片下方的文字](图片地址 "图片title")

    其中title可加可不加,它就是鼠标移动到图片上时显示的文字。
    然而我在使用hexo搭建我的个人博客的过程中,遇到了使用上述语法图片却无法显示的情况,因此我改用了下列标签插件:

    1
    {% asset_img xxxxx.xxx 图片下方的名字 %}

    其中xxxxx.xxx只需直接输入图片名称以及格式即可,因为我使用了hexo-asset-image插件,它可以在_posts文件中创建与博文名称相同的对应的文件夹,只需把图片移入即可。注意,这里的图片名中间不能有空格,否则会加载失败(它会以为图片名称到第一个空格为止)。
    其安装命令:npm install hexo-asset-image --save
    也可用cnpm更快地安装:cnpm install hexo-asset-image --save

    补充:后来发现在关于本人中无法使用上述asset_img标签插件来对图片进行插入,故又尝试了![显示在图片下方的文字](图片地址 "图片title")的方法,发现可行!原因可能是之前误用了中文括号导致的。可以参考一下Hexo文章中插入图片的方法

    在插入图片的后面,会留有一小段空白区,看着不舒服的话可以不要回车,即直接在插入图片的语句后面跟进下一段的文字或者图片等,这样行间隙就会小很多。
    其实在hexo中可以直接使用img标签,它会自行处理,并且这样还更方便调整高度和宽度。

    1
    <img src="" width="50%" height="50%">
  6. 超链接

    由于我希望在新的页面打开链接,而似乎markdown本身的语法不支持在新标签页打开链接,因此我推荐直接使用html语言来代替。

    1
    <a href="超链接地址" target="_blank">超链接名</a>
  7. 列表

    • 无序列表

      1
      2
      3
      - 列表内容
      + 列表内容
      * 列表内容
      • 列表内容
      • 列表内容
      • 列表内容
    • 有序列表

      1
      2
      3
      1. 列表内容
      2. 列表内容
      3. 列表内容
      1. 列表内容
      2. 列表内容
      3. 列表内容
        可以看到,上面显示的列表是有嵌套的,方法就是敲三个空格缩进。
  8. 表格

    1
    2
    3
    4
    表头|表头|表头
    ---|:--:|---:
    内容|内容|内容
    内容|内容|内容

    其中第二行的作用分割表头和内容,-有一个就行,为了对齐可多加几个。
    此外文字默认居左,有两种改变方法:
    两边加:表示文字居中。
    右边加:表示文字居右。
    然而我在hexo使用表格时,出现了无法正常转换的问题,因此我改用了如下HTML的表格形式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <table border="1">
    <tr>
    <td>第一行第一列</td>
    <td>第一行第二列</td>
    </tr>
    <tr>
    <td>第二行第一列</td>
    <td>第三行第二列</td>
    </tr>
    </table>

    效果如下:

    第一行第一列第一行第二列
    第二行第一列第二行第二列
  9. 代码

    最后的最后,是我最喜欢ctrl+C+V的代码了。
    单行或句中代码输入方式:

    1
    `来复制我呀`

    显示:
    来复制我呀
    其中“`”在键盘的左上角,我当初找了好久。
    多行代码块的写法就是用上下两对“```”围住。
    好了于是你现在就可以自由的复制粘贴啦。


碰到底线咯 后面没有啦

本文标题:markdown笔记:markdown的基本使用

文章作者:高深远

发布时间:2019年09月13日 - 21:11

最后更新:2020年02月07日 - 16:51

原始链接:https://gsy00517.github.io/markdown20190913211144/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
markdown笔记:markdown的基本使用 | 高深远的博客

markdown笔记:markdown的基本使用

既然要写技术博客,那么markdown肯定是必备的了,这篇文章就来介绍一下markdown的基本使用操作。

References

电子文献:
https://www.jianshu.com/p/191d1e21f7ed


介绍

Markdown是一种可以使用普通文本编辑器编写的标记语言,其功能比纯文本更强,因此许多程序员用它来写blog。在这里我先推荐一款markdown编辑器——typora,大家可以免费下载使用。


注意

在我刚开始使用markdown的时候总是跳进这个坑,在这里提上来提醒一下,在使用markdown标记后要添加文字时,需要在相应标记后空一格,否则标记也会被当作文本来处理,例如我输入“#####错误”时:

错误

正确的做法是输入“##### 正确”:

正确

一种简单的判别方法就是使用IDE,这样对应的标记就会有语法高亮。


使用

  1. 标题

    话不多说,直接示范:

    1
    2
    3
    4
    5
    6
    # 这是一级标题
    ## 这是二级标题
    ### 这是三级标题
    #### 这是四级标题
    ##### 这是五级标题
    ###### 这是六级标题

    效果如下:

    这是一级标题

    这是二级标题

    这是三级标题

    这是四级标题

    这是五级标题
    这是六级标题
  2. 字体

    还是直接示范:

    1
    2
    3
    4
    **这是加粗的文字**
    *这是倾斜的文字*`
    ***这是斜体加粗的文字***
    ~~这是加删除线的文字~~

    这是加粗的文字
    这是倾斜的文字
    这是斜体加粗的文字
    这是加删除线的文字

  3. 引用

    1
    2
    3
    4
    >我引用
    >>我还引用
    >>>我再引用
    >>>>>>>>>>>>>扶我起来,我还能继续引用!

    我引用

    我还引用

    我再引用

    扶我起来,我还能继续引用!

    引用是可以嵌套的,可以加很多层,我一般使用一个>来表示额外的需要注意的内容。另外,如果想让下一段文字不被引用,需要空一行。

  4. 分割线

    分割线使用三个及以上的-*就可以。
    有时候用---会造成别的文字的格式变化,因此我在使用VScode编辑时,如果看到---被高亮(分割线正常其作用时应该不高亮),就会改用***

    1
    2
    ---
    ***

    效果如下:



  5. 图片

    markdown中添加图片的语法是这样的:

    1
    ![显示在图片下方的文字](图片地址 "图片title")

    其中title可加可不加,它就是鼠标移动到图片上时显示的文字。
    然而我在使用hexo搭建我的个人博客的过程中,遇到了使用上述语法图片却无法显示的情况,因此我改用了下列标签插件:

    1
    {% asset_img xxxxx.xxx 图片下方的名字 %}

    其中xxxxx.xxx只需直接输入图片名称以及格式即可,因为我使用了hexo-asset-image插件,它可以在_posts文件中创建与博文名称相同的对应的文件夹,只需把图片移入即可。注意,这里的图片名中间不能有空格,否则会加载失败(它会以为图片名称到第一个空格为止)。
    其安装命令:npm install hexo-asset-image --save
    也可用cnpm更快地安装:cnpm install hexo-asset-image --save

    补充:后来发现在关于本人中无法使用上述asset_img标签插件来对图片进行插入,故又尝试了![显示在图片下方的文字](图片地址 "图片title")的方法,发现可行!原因可能是之前误用了中文括号导致的。可以参考一下Hexo文章中插入图片的方法

    在插入图片的后面,会留有一小段空白区,看着不舒服的话可以不要回车,即直接在插入图片的语句后面跟进下一段的文字或者图片等,这样行间隙就会小很多。
    其实在hexo中可以直接使用img标签,它会自行处理,并且这样还更方便调整高度和宽度。

    1
    <img src="" width="50%" height="50%">
  6. 超链接

    由于我希望在新的页面打开链接,而似乎markdown本身的语法不支持在新标签页打开链接,因此我推荐直接使用html语言来代替。

    1
    <a href="超链接地址" target="_blank">超链接名</a>
  7. 列表

    • 无序列表

      1
      2
      3
      - 列表内容
      + 列表内容
      * 列表内容
      • 列表内容
      • 列表内容
      • 列表内容
    • 有序列表

      1
      2
      3
      1. 列表内容
      2. 列表内容
      3. 列表内容
      1. 列表内容
      2. 列表内容
      3. 列表内容
        可以看到,上面显示的列表是有嵌套的,方法就是敲三个空格缩进。
  8. 表格

    1
    2
    3
    4
    表头|表头|表头
    ---|:--:|---:
    内容|内容|内容
    内容|内容|内容

    其中第二行的作用分割表头和内容,-有一个就行,为了对齐可多加几个。
    此外文字默认居左,有两种改变方法:
    两边加:表示文字居中。
    右边加:表示文字居右。
    然而我在hexo使用表格时,出现了无法正常转换的问题,因此我改用了如下HTML的表格形式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <table border="1">
    <tr>
    <td>第一行第一列</td>
    <td>第一行第二列</td>
    </tr>
    <tr>
    <td>第二行第一列</td>
    <td>第三行第二列</td>
    </tr>
    </table>

    效果如下:

    第一行第一列第一行第二列
    第二行第一列第二行第二列
  9. 代码

    最后的最后,是我最喜欢ctrl+C+V的代码了。
    单行或句中代码输入方式:

    1
    `来复制我呀`

    显示:
    来复制我呀
    其中“`”在键盘的左上角,我当初找了好久。
    多行代码块的写法就是用上下两对“```”围住。
    好了于是你现在就可以自由的复制粘贴啦。


碰到底线咯 后面没有啦

本文标题:markdown笔记:markdown的基本使用

文章作者:高深远

发布时间:2019年09月13日 - 21:11

最后更新:2020年02月07日 - 16:51

原始链接:https://gsy00517.github.io/markdown20190913211144/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
markdown笔记:公式插入和代码高亮 | 高深远的博客

markdown笔记:公式插入和代码高亮

在上一篇文章deep-learning笔记:着眼于深度——VGG简介与pytorch实现中,我用到了markdown其他的一些使用方法,因此我想在此对之前的一篇文章markdown笔记:markdown的基本使用做一些补充。

References

电子文献:
https://www.jianshu.com/p/25f0139637b7
https://www.jianshu.com/p/fd97e1f8f699
https://www.jianshu.com/p/68e6f82d88b7
https://www.jianshu.com/p/7c02c112d532


公式插入

无论是学习ML还是DL,我们总是离不开数学的,于是利用markdown插入数学公式就成了一个的需求。那么怎么在markdown中插入公式呢?
markdown中的公式分为两类,即行内公式与行间公式。它们对应的代码如下:

1
2
$ \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. $
$$\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.$$

让我们来看一下效果:
行内公式:$ \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. $
行间公式:

如果你是使用hexo编写博客,那么默认的设置是无法转义markdown公式的,解决这个问题的配置方法可以参考本文顶部给出的第三个链接。
另外要注意,在使用公式时,对应文件需开启mathjax选项。

补充更新:在查看next主题配置文件时,我注意到next好像自带mathjax支持,设置如下,这样就无需在每个文件中添加开启mathjax的选项。

markdown公式的具体语法可以参照本文的第一个链接,你可以在typora中根据它的官方文档进行尝试。

注意:在typora中,只需输入$或者$$就可直接进入公式编辑,无需输入一对。

有机会我再对上面提到的语法进行搬运。下面介绍一种更简单省力的方法(也是我在用的方法):

  1. 打开在线LaTex公式编辑器
  2. 在上方的框框中输入你想要的公式: 你可以在下方的GIF图中随时观察你的输入时候符合预期,如在书写word文档等类似文本时需要插入公式,也可以直接复制图片。
  3. 拷贝下方黄颜色方框中的代码到markdown文件。 你可以选择去掉两边的“\”和方括号,否则你的公式两侧将会套有方括号,另外你还需要使用上文提到的$来确定公式显示方式。
    这里我们这样输入:$ x+y=z $
    得到:$ x+y=z $。
    以上就是使用LaTex给markdown添加公式的方法。
    你也可以使用黄颜色框中的URL选项来添加代码,格式是![](URL)
    例如,输入:![](https://latex.codecogs.com/gif.latex?x&plus;y=z)
    可以看到:
    这种方法就不需要文章顶部链接三中的配置了,也是一种推荐的方法。

    注:若在在线LaTex公式编辑器中找不到需要的元素或者符号的话,可以看一看LaTex常用公式整理


代码高亮

markdown中使代码高亮的格式如下:

1
2
3
三个反引号+语言名
代码...
三个反引号

例如,输入:

可以看到:

1
print("hello world!")

同样的,在typora中,你也不必输入成对的三个反引号。
这里我要提醒一个我以前用Rmarkdown时踩过的坑:

注意!他俩是不一样的!

真正的“`”在这里:


碰到底线咯 后面没有啦

本文标题:markdown笔记:公式插入和代码高亮

文章作者:高深远

发布时间:2019年09月15日 - 09:56

最后更新:2020年02月15日 - 08:09

原始链接:https://gsy00517.github.io/markdown20190915095628/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
markdown笔记:公式插入和代码高亮 | 高深远的博客

markdown笔记:公式插入和代码高亮

在上一篇文章deep-learning笔记:着眼于深度——VGG简介与pytorch实现中,我用到了markdown其他的一些使用方法,因此我想在此对之前的一篇文章markdown笔记:markdown的基本使用做一些补充。

References

电子文献:
https://www.jianshu.com/p/25f0139637b7
https://www.jianshu.com/p/fd97e1f8f699
https://www.jianshu.com/p/68e6f82d88b7
https://www.jianshu.com/p/7c02c112d532


公式插入

无论是学习ML还是DL,我们总是离不开数学的,于是利用markdown插入数学公式就成了一个的需求。那么怎么在markdown中插入公式呢?
markdown中的公式分为两类,即行内公式与行间公式。它们对应的代码如下:

1
2
$ \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. $
$$\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.$$

让我们来看一下效果:
行内公式:$ \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. $
行间公式:

如果你是使用hexo编写博客,那么默认的设置是无法转义markdown公式的,解决这个问题的配置方法可以参考本文顶部给出的第三个链接。
另外要注意,在使用公式时,对应文件需开启mathjax选项。

补充更新:在查看next主题配置文件时,我注意到next好像自带mathjax支持,设置如下,这样就无需在每个文件中添加开启mathjax的选项。

markdown公式的具体语法可以参照本文的第一个链接,你可以在typora中根据它的官方文档进行尝试。

注意:在typora中,只需输入$或者$$就可直接进入公式编辑,无需输入一对。

有机会我再对上面提到的语法进行搬运。下面介绍一种更简单省力的方法(也是我在用的方法):

  1. 打开在线LaTex公式编辑器
  2. 在上方的框框中输入你想要的公式: 你可以在下方的GIF图中随时观察你的输入时候符合预期,如在书写word文档等类似文本时需要插入公式,也可以直接复制图片。
  3. 拷贝下方黄颜色方框中的代码到markdown文件。 你可以选择去掉两边的“\”和方括号,否则你的公式两侧将会套有方括号,另外你还需要使用上文提到的$来确定公式显示方式。
    这里我们这样输入:$ x+y=z $
    得到:$ x+y=z $。
    以上就是使用LaTex给markdown添加公式的方法。
    你也可以使用黄颜色框中的URL选项来添加代码,格式是![](URL)
    例如,输入:![](https://latex.codecogs.com/gif.latex?x&plus;y=z)
    可以看到:
    这种方法就不需要文章顶部链接三中的配置了,也是一种推荐的方法。

    注:若在在线LaTex公式编辑器中找不到需要的元素或者符号的话,可以看一看LaTex常用公式整理


代码高亮

markdown中使代码高亮的格式如下:

1
2
3
三个反引号+语言名
代码...
三个反引号

例如,输入:

可以看到:

1
print("hello world!")

同样的,在typora中,你也不必输入成对的三个反引号。
这里我要提醒一个我以前用Rmarkdown时踩过的坑:

注意!他俩是不一样的!

真正的“`”在这里:


碰到底线咯 后面没有啦

本文标题:markdown笔记:公式插入和代码高亮

文章作者:高深远

发布时间:2019年09月15日 - 09:56

最后更新:2020年02月15日 - 08:09

原始链接:https://gsy00517.github.io/markdown20190915095628/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
markdown笔记:写报告时的一些应用 | 高深远的博客

markdown笔记:写报告时的一些应用

在上一篇matlab笔记:久未使用之后踩的一堆坑中,我用matlab完成了实验,接下来就是写报告了。然而写博客用惯了markdown,现在极其嫌弃word文档。考虑到这次报告中大量的公式及代码,word文档的观感可想而知,因此果断决定使用markdown来书写实验报告。那么这篇文章就及时跟进一下我在写报告的时候发现的一些之前没注意的新应用吧。

References

电子文献:
https://blog.csdn.net/m0_37925202/article/details/80461714
https://www.w3school.com.cn/tags/att_p_align.asp


居中文字

之前写markdown的时候一直没有考虑到要把文字居中,而这回报告的标题就有这个要求了。
实现的方法有如下两种。

1
<center>这样就居中了</center>

1
<p align="center">这样就居中了</p>

此外html中的<p>标签的align属性还有其它的用法:

1
2
3
4
<p align="left"> <!--左对齐-->
<p align="right"> <!--右对齐-->
<p align="center"> <!--居中-->
<p align="justify"> <!--对行进行伸展,使每行都可以有相等的长度(就像在报纸和杂志中)-->


转pdf文件

考虑到markdown文件不能作为最终的报告提交,我就想办法将markdown转化为pdf。
我首先找到了VScode里面的markdown PDF扩展,然而安装之后有一个东西一直安装失败。
这里我想起了之前我在markdown笔记:markdown的基本使用中强烈推荐过的typora(我的报告最后就是用它书写的),打开之后,果然没有让我失望。直接点击“文件”“导出”选择PDF,瞬间就完成了pdf文件的转换。此外,typora提供的导出格式实在是丰富,虽然导出的word与原markdown文件的颜值有少许降低,但还是不得不赞叹typora的实用!


行间公式

刚开始使用typora时,我遇到了一个疑惑:似乎typora不能插入行内公式块。在查找相关资料之后,我终于找到了解决的方案。
点击“文件”“偏好设置”,把markdown扩展语法中的内联公式项打上勾。

补充:当使用$夹着文字而非LaTex格式的公式时,字体会发生变化,如输入$我会变$我没变,呈现的效果是:$我会变$我没变。


插入图片

typora插入图片的方法与hexo中类似,也是可以创建一个文件夹存放图片,然后在偏好设置里进行设置。当指定路径之后,typora存放图片的文件夹名可以与markdown文件的名字不一致,而hexo中则需要一致才能够直接用图片名调用。

此外,还是在偏好设置中,我们可以调整字体,我一般使用的是16px。


碰到底线咯 后面没有啦

本文标题:markdown笔记:写报告时的一些应用

文章作者:高深远

发布时间:2019年11月10日 - 19:42

最后更新:2020年02月07日 - 17:32

原始链接:https://gsy00517.github.io/markdown20191110194256/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
markdown笔记:写报告时的一些应用 | 高深远的博客

markdown笔记:写报告时的一些应用

在上一篇matlab笔记:久未使用之后踩的一堆坑中,我用matlab完成了实验,接下来就是写报告了。然而写博客用惯了markdown,现在极其嫌弃word文档。考虑到这次报告中大量的公式及代码,word文档的观感可想而知,因此果断决定使用markdown来书写实验报告。那么这篇文章就及时跟进一下我在写报告的时候发现的一些之前没注意的新应用吧。

References

电子文献:
https://blog.csdn.net/m0_37925202/article/details/80461714
https://www.w3school.com.cn/tags/att_p_align.asp


居中文字

之前写markdown的时候一直没有考虑到要把文字居中,而这回报告的标题就有这个要求了。
实现的方法有如下两种。

1
<center>这样就居中了</center>

1
<p align="center">这样就居中了</p>

此外html中的<p>标签的align属性还有其它的用法:

1
2
3
4
<p align="left"> <!--左对齐-->
<p align="right"> <!--右对齐-->
<p align="center"> <!--居中-->
<p align="justify"> <!--对行进行伸展,使每行都可以有相等的长度(就像在报纸和杂志中)-->


转pdf文件

考虑到markdown文件不能作为最终的报告提交,我就想办法将markdown转化为pdf。
我首先找到了VScode里面的markdown PDF扩展,然而安装之后有一个东西一直安装失败。
这里我想起了之前我在markdown笔记:markdown的基本使用中强烈推荐过的typora(我的报告最后就是用它书写的),打开之后,果然没有让我失望。直接点击“文件”“导出”选择PDF,瞬间就完成了pdf文件的转换。此外,typora提供的导出格式实在是丰富,虽然导出的word与原markdown文件的颜值有少许降低,但还是不得不赞叹typora的实用!


行间公式

刚开始使用typora时,我遇到了一个疑惑:似乎typora不能插入行内公式块。在查找相关资料之后,我终于找到了解决的方案。
点击“文件”“偏好设置”,把markdown扩展语法中的内联公式项打上勾。

补充:当使用$夹着文字而非LaTex格式的公式时,字体会发生变化,如输入$我会变$我没变,呈现的效果是:$我会变$我没变。


插入图片

typora插入图片的方法与hexo中类似,也是可以创建一个文件夹存放图片,然后在偏好设置里进行设置。当指定路径之后,typora存放图片的文件夹名可以与markdown文件的名字不一致,而hexo中则需要一致才能够直接用图片名调用。

此外,还是在偏好设置中,我们可以调整字体,我一般使用的是16px。


碰到底线咯 后面没有啦

本文标题:markdown笔记:写报告时的一些应用

文章作者:高深远

发布时间:2019年11月10日 - 19:42

最后更新:2020年02月07日 - 17:32

原始链接:https://gsy00517.github.io/markdown20191110194256/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:久未使用之后踩的一堆坑 | 高深远的博客

matlab笔记:久未使用之后踩的一堆坑

本周科学计算引论结课了,就花了一整天时间把要求的实验报告写了。根据考核说明,算法可以使用各种工具、语言来实现,但由于这门课程的上机实验统一使用的是matlab,再加上我上一次使用matlab大量实践是在劳动节的数学建模华中赛了,还是很有必要重拾起来再熟悉一下。于是乎,一大波坑就等着我去填了。

References

电子文献:
https://blog.csdn.net/zhanshen112/article/details/79728887


报错:未找到具有匹配签名的构造函数

我编写了一个二分法的函数求解非线性方程,然而当我调用的时候,却遇到报错:“未找到具有匹配签名的构造函数”,这是怎么回事呢?
我们来冷静分析一下。

识破!
原来是我定义的函数名为half,而half也是matlab自带的函数之一,可以使用help functionname查看函数具体的使用方法与功能。

调用时默认优先使用自带的函数,因此修改函数名为matlab自带函数之外的即可。


报错:矩阵维度必须一致

这是我在画图时遇到的一个问题,首先先补充一下matlab中作函数图像的方法,如下图所示。

当我尝试使用上述方法作简单的函数图像时,并没有报错,而当我想要作出我实验所需函数(如下所示)的图像时,却出现了“矩阵维度必须一致”的错误信息。

1
y = asin(1 / (1.878 + 0.75 * cos(x)))

查阅相关资料,我才发现是乘除的时候出现的问题,为此我将其中的除/修改为乘*,并用.^代替^作乘方计算,即将原函数式修改为:

1
y = asin(1 * (1.878 + 0.75 * cos(x)) .^ (-1))

这样,问题就解决了。
这里我想强调一下在matlab中^.^的区别:.^是点乘,而^是乘法。
直接用^进行乘法的话,在这里即矩阵乘法,也就是说,必须满足前一个矩阵的列数等于后一个矩阵的行数。
而使用.^点乘操作,是使每一个元素相乘,也就是向量或者矩阵中对应元素相乘,也很好记忆,加个点就是点乘。


函数输出只有一个

这是一个很愚蠢的问题,显然我好久没用了,因为matlab不必使用return返回结果,在函数声明的第一句就确定了返回值的数量和顺序。因此在调用函数的时候,必须也提供对应的变量去接收返回值,否则只能得到第一个返回的元素。


使用diff求导不是导数值

用惯了pytorch,总想着能够自动求导,一查matlab还真有这么一个函数,即diff函数。然而,事实证明它不是我想要的。
我们可以在命令行中使用这一个函数:

  1. 声明变量x:syms x。它代表着声明符号变量x,只有声明了符号变量才可以进行符号运算,包括求导。
  2. 定义一个需要求导的函数:f(x) = sin(x) + x ^ 2
  3. 使用diff函数求导:diff(f(x))。也可以对已经定义好的m文件中的函数直接求导。
    这里,我们会得到ans为2*x + cos(x)
  4. 如果想pretty一些,可以使用pretty函数将结果转化成书面格式:pretty(ans)

然而,当我代入不同的具体数值想得到函数的导数值的时候,发现输出的结果却是0。
使用help查阅diff函数的用法,得到的说明是:

1
2
3
4
此MATLAB函数计算沿大小不等于1的第一个数组维度的X相邻元素之间的差分:
Y = diff(X)
Y = diff(X,n)
Y = diff(X,n,dim)

原来这个函数的主要用法是对向量或者矩阵中的元素进行差分计算,当用它来求导时,得到的只是一个表达式,且函数一但复杂,得到的就是一个参数众多的逼近格式。
唉,总而言之,还是老老实实地拿起笔自己算出导函数吧。都用矩阵实验室(matlab)了,手动求个导还是得会的呀。


角度制弧度制互换

无论使用计算器还是编程计算,这都是一个需要注意的点,matlab默认使用的是弧度制,在计算出结果之后,可以使用rad2deg函数进行转换。
同样的,我推测角度制转弧度制的函数名为deg2rad,一试,果不其然。这个函数还是挺好记的,英文里有许多同音词与字符的妙用,比如这里的to和2的two,还有at和@,感觉既方便又高级。


保留更多位数

matlab默认是保留4位有效数字,为了提升计算精度,可以使用format long来增加计算过程中保留的位数。


常用操作

难得开一篇写matlab使用的博文,那就在这补上几个我记忆中的较为常用的命令或操作。

  1. Ctrl+C终止操作。这跟许多地方都一样,在matlab中,Ctrl+C平时可以用来粘贴剪切板上的内容,而在程序运行时,可以使用它来终止运行,这在死循环的时候非常有用。
  2. clc清空命令行。
  3. bench测试性能。这其实不是一个很常用的命令,可以跟朋友输这个命令看看自己电脑的性能,下图是我的结果。需要注意的是,笔记本电脑电池使用模式的不同对这个排名影响还是挺大的,如果想让排名高一些的话,请确保电池开在最佳性能的模式。另外,不同的电脑的比较对象可能会不一样,比如学校的台式机和我的笔记本在这里比较的对象就不一样。 常用的还有许多,以后在使用中不断增加,先在这占个位。
    最近在b站看到matlab的几个有趣彩蛋,挺有意思,感兴趣可以去瞧瞧。

碰到底线咯 后面没有啦

本文标题:matlab笔记:久未使用之后踩的一堆坑

文章作者:高深远

发布时间:2019年11月10日 - 19:36

最后更新:2020年02月07日 - 17:31

原始链接:https://gsy00517.github.io/matlab20191110193640/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:久未使用之后踩的一堆坑 | 高深远的博客

matlab笔记:久未使用之后踩的一堆坑

本周科学计算引论结课了,就花了一整天时间把要求的实验报告写了。根据考核说明,算法可以使用各种工具、语言来实现,但由于这门课程的上机实验统一使用的是matlab,再加上我上一次使用matlab大量实践是在劳动节的数学建模华中赛了,还是很有必要重拾起来再熟悉一下。于是乎,一大波坑就等着我去填了。

References

电子文献:
https://blog.csdn.net/zhanshen112/article/details/79728887


报错:未找到具有匹配签名的构造函数

我编写了一个二分法的函数求解非线性方程,然而当我调用的时候,却遇到报错:“未找到具有匹配签名的构造函数”,这是怎么回事呢?
我们来冷静分析一下。

识破!
原来是我定义的函数名为half,而half也是matlab自带的函数之一,可以使用help functionname查看函数具体的使用方法与功能。

调用时默认优先使用自带的函数,因此修改函数名为matlab自带函数之外的即可。


报错:矩阵维度必须一致

这是我在画图时遇到的一个问题,首先先补充一下matlab中作函数图像的方法,如下图所示。

当我尝试使用上述方法作简单的函数图像时,并没有报错,而当我想要作出我实验所需函数(如下所示)的图像时,却出现了“矩阵维度必须一致”的错误信息。

1
y = asin(1 / (1.878 + 0.75 * cos(x)))

查阅相关资料,我才发现是乘除的时候出现的问题,为此我将其中的除/修改为乘*,并用.^代替^作乘方计算,即将原函数式修改为:

1
y = asin(1 * (1.878 + 0.75 * cos(x)) .^ (-1))

这样,问题就解决了。
这里我想强调一下在matlab中^.^的区别:.^是点乘,而^是乘法。
直接用^进行乘法的话,在这里即矩阵乘法,也就是说,必须满足前一个矩阵的列数等于后一个矩阵的行数。
而使用.^点乘操作,是使每一个元素相乘,也就是向量或者矩阵中对应元素相乘,也很好记忆,加个点就是点乘。


函数输出只有一个

这是一个很愚蠢的问题,显然我好久没用了,因为matlab不必使用return返回结果,在函数声明的第一句就确定了返回值的数量和顺序。因此在调用函数的时候,必须也提供对应的变量去接收返回值,否则只能得到第一个返回的元素。


使用diff求导不是导数值

用惯了pytorch,总想着能够自动求导,一查matlab还真有这么一个函数,即diff函数。然而,事实证明它不是我想要的。
我们可以在命令行中使用这一个函数:

  1. 声明变量x:syms x。它代表着声明符号变量x,只有声明了符号变量才可以进行符号运算,包括求导。
  2. 定义一个需要求导的函数:f(x) = sin(x) + x ^ 2
  3. 使用diff函数求导:diff(f(x))。也可以对已经定义好的m文件中的函数直接求导。
    这里,我们会得到ans为2*x + cos(x)
  4. 如果想pretty一些,可以使用pretty函数将结果转化成书面格式:pretty(ans)

然而,当我代入不同的具体数值想得到函数的导数值的时候,发现输出的结果却是0。
使用help查阅diff函数的用法,得到的说明是:

1
2
3
4
此MATLAB函数计算沿大小不等于1的第一个数组维度的X相邻元素之间的差分:
Y = diff(X)
Y = diff(X,n)
Y = diff(X,n,dim)

原来这个函数的主要用法是对向量或者矩阵中的元素进行差分计算,当用它来求导时,得到的只是一个表达式,且函数一但复杂,得到的就是一个参数众多的逼近格式。
唉,总而言之,还是老老实实地拿起笔自己算出导函数吧。都用矩阵实验室(matlab)了,手动求个导还是得会的呀。


角度制弧度制互换

无论使用计算器还是编程计算,这都是一个需要注意的点,matlab默认使用的是弧度制,在计算出结果之后,可以使用rad2deg函数进行转换。
同样的,我推测角度制转弧度制的函数名为deg2rad,一试,果不其然。这个函数还是挺好记的,英文里有许多同音词与字符的妙用,比如这里的to和2的two,还有at和@,感觉既方便又高级。


保留更多位数

matlab默认是保留4位有效数字,为了提升计算精度,可以使用format long来增加计算过程中保留的位数。


常用操作

难得开一篇写matlab使用的博文,那就在这补上几个我记忆中的较为常用的命令或操作。

  1. Ctrl+C终止操作。这跟许多地方都一样,在matlab中,Ctrl+C平时可以用来粘贴剪切板上的内容,而在程序运行时,可以使用它来终止运行,这在死循环的时候非常有用。
  2. clc清空命令行。
  3. bench测试性能。这其实不是一个很常用的命令,可以跟朋友输这个命令看看自己电脑的性能,下图是我的结果。需要注意的是,笔记本电脑电池使用模式的不同对这个排名影响还是挺大的,如果想让排名高一些的话,请确保电池开在最佳性能的模式。另外,不同的电脑的比较对象可能会不一样,比如学校的台式机和我的笔记本在这里比较的对象就不一样。 常用的还有许多,以后在使用中不断增加,先在这占个位。
    最近在b站看到matlab的几个有趣彩蛋,挺有意思,感兴趣可以去瞧瞧。

碰到底线咯 后面没有啦

本文标题:matlab笔记:久未使用之后踩的一堆坑

文章作者:高深远

发布时间:2019年11月10日 - 19:36

最后更新:2020年02月07日 - 17:31

原始链接:https://gsy00517.github.io/matlab20191110193640/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:一个非线性方程问题的多种求解方法 | 高深远的博客

matlab笔记:一个非线性方程问题的多种求解方法

上周科学计算引论结课了,借着写上机报告的机会,我把书本上所有的求解非线性方程的方法(标量型)都用matlab实现了一下,并对一个实际工程问题进行求解。关于实现过程中遇到的问题以及注意事项,我已写在matlab笔记:久未使用之后踩的一堆坑内。


实际问题

在电机学中,凸极同步发电机的功角特性可表示为:

式中,$P_{em}$表示发电机的电磁功率;$E_{0}$表示发电机电势;$V$表示发电机端电压;$x_{q}$表示横轴同步电抗;$x_{d}$表示纵轴同步电抗;$\theta$表示功率角,$\theta \in \left ( 0,\frac{\pi }{2} \right )$。
如令$\frac{E_{0}V}{x_{d}}=P_{j}$,$V^{2}\left ( \frac{1}{x_{q}}-\frac{1}{x_{d}} \right )=P_{2e}$,则上式可以简化为:

在电力系统稳定计算中,我们往往要由上式求出功率角$\theta$。我们可以使用几何方法求解,也可以利用迭代法求解该非线性方程。
我们将上式变为:

以许实章编《电机学习题集》第367页26-1为例,将$P_{em}=1$,$P_{j}=1.878$,$P_{2e}=0.75$代入,得到方程:


问题求解

在《计算方法》第二章,我们学习了一些非线性方程的数值解法,这里我们分别使用几何法和迭代法求解上述问题并进行比较。设方程求解的预定精度为$0.001^{\circ}$即$1.7\times 10^{-3}rad$,由闭区间上连续函数的性质和初步估计,可确定方程的解位于区间$[0,\frac{\pi }{6}]$。

几何方法

由几何法的求解方法,我们定义函数:

求解$\theta$即求解$f(\theta )$在$[0,\frac{\pi }{6}]$上的零点。
在matlab中,将该函数实现如下:

1
2
3
4
5
6
function [y] = func(x)
%几何法函数方程
%统一使用弧度制
format long %为提高精度,保留更多位数
y = asin(1 / (1.878 + 0.75 * cos(x))) - x;
end

由于matlab计算过程中默认保留$4$位小数,为了提高精度,我使用了format long保留更多有效数字。值得注意的是,计算时统一使用弧度制,待求出解之后再使用rad2deg函数转化为角度。

二分法

根据二分法的格式,编写二分法函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function [errors, ans, time] = bisection(low, high, max, stopError) 
%二分法
%func是待求零点的函数
%low,high分别是解区间的上下限
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
fl = func(low);
fh = func(high);
error = high - low;
for i = 1 : max
mid = (low + high ) / 2;
fm = func(mid);
if fm * fl > 0
low = mid;
else high = mid;
end
error = high - low;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(mid); %转换成角度
end

输入命令[errorsB, ans, time] = bisection(0, pi / 6, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
errorsB =

列 1 至 5

0.261799387799149 0.130899693899575 0.065449846949787 0.032724923474894 0.016362461737447

列 6 至 10

0.008181230868723 0.004090615434362 0.002045307717181 0.001022653858590 0.000511326929295

列 11 至 15

0.000255663464648 0.000127831732324 0.000063915866162 0.000031957933081 0.000015978966540


ans =

22.909240722656250


time =

15

使用二分法共迭代$15$次,求得结果为$22.909240722656250^{\circ}$。此外,迭代误差为$0.000015978966540$,符合预设精度要求。然而,此种方法计算次数较多,因此我又尝试了下面的方法。

弦截法

根据弦截法的计算方法,编写弦截法函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function [errors, ans, time] = linecut(a, b, max, stopError)
%弦截法
%func是待求零点的函数
%a,b是在解的领域取的两点,这里取解区间的两个端点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
fa = func(a);
fb = func(b);
error = abs(a - b);
for i = 1 : max
x = b - (b - a) * fb / (fb - fa);
a = b;
b = x;
fa = func(a);
fb = func(b);
error = abs(a - b);
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(b); %转换成角度
end

输入命令[errorsL, ans, time] = linecut(0, pi / 6, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsL =

0.120609794441825 0.003164447033051 0.000026217912123 0.000000005427336


ans =

22.909760215805360


time =

4

与上面的二分法相比,弦截法只需计算$4$次,效率大大提高。计算所得结果为$22.909760215805360^{\circ}$,迭代误差为$0.000000005427336$,符合预设精度要求,而且比二分法的最终迭代误差更小,显然可以发现弦截法的收敛速度要快于二分法。下面我再用弦截法的改造方法Steffensen方法进行试验。

Steffensen方法

根据Steffensen方法的计算方法,编写函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function [errors, ans, time] = steffensen(x, max, stopError)
%Steffensen方法
%func是待求零点的函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
f = func(x);
for i = 1 : max
o = x - f ^ 2 / (f - func(x - f));
error = abs(x - o);
x = o;
f = func(o);
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(o); %转换成角度
end

选取初始点为$0$,输入命令[errorsS1, ans, time] = steffensen(0, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsS1 =

0.381516143583955 0.018291709371805 0.000042893415627 0.000000000236814


ans =

22.909760215804820


time =

4

发现计算次数没有比弦截法少,因此修改初值为$\frac{\pi }{6}$,再次计算,得结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsS2 =

0.125855705283236 0.002107105082521 0.000000571210576


ans =

22.909760215802425


time =

3

一般而言,Steffensen方法的收敛速度要快于弦截法,但这与初始点的选取有关。对于这个问题,当设初始点为$0$时,Steffensen方法的迭代次数与弦截法持平;当设初始点为$\frac{\pi }{6}$时,迭代次数才小于弦截法。因此,想让Steffensen方法更快地收敛,需选取合适的初始点。在我看来,Steffensen方法的一大优势就是它是一种单步迭代方法,相比二步迭代方法的弦截法,Steffensen方法只需要一个初值就可以开始迭代。

Picard迭代法

上述的方法都是基于几何图形的求解方法,而下面的Picard迭代法则是基于不动点原理给出的。
首先,我们编写迭代函数:

1
2
3
4
5
6
function [y] = interation(x)
%迭代函数
%统一使用弧度制
format long %为提高精度,保留更多位数
y = asin(1 / (1.878 + 0.75 * cos(x)));
end

我们使用如下命令查看函数图像:

1
2
3
>> x = 0 : pi / 36 : pi / 6;
>> y = asin(1 * (1.878 + 0.75 * cos(x)) .^ (-1));
>> plot(x, y)

得到:

易验证,该函数在$[0,\frac{\pi }{6}]$上满足Picard迭代条件。

Picard迭代法

根据Picard迭代法原理,定义Picard迭代函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function [errors, ans, time] = picard(x, max, stopError)
%Picard迭代法
%interation是迭代函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
o = interation(x);
error = abs(x - o);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(o); %转换成角度
end

输入命令[errorsP, ans, time] = picard(0, 50, 1.7e-5)求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsP =

0.390355832559508 0.009044503640668 0.000428788831489 0.000020583068808 0.000000988625736


ans =

22.909757357777192


time =

5

Picard迭代结果为$22.909757357777192^{\circ}$,且精度符合要求。下面使用Picard迭代法的改进Aitken加速迭代法进行试验。

Aitken加速迭代法

编写Aitken加速迭代法函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function [errors, ans, time] = aitken(x, max, stopError)
%Aitken迭代法
%interation是迭代函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
x1 = interation(x);
x2 = interation(x1);
x3 = x2 - (x2 - x1) ^ 2 / (x2 - 2 * x1 + x);
error = abs(x3 - x);
x = x3;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

输入命令[errorsA, ans, time] = aitken(0, 50, 1.7e-5)求解得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsA =

0.399614867056990 0.000235879375045 0.000000000176165


ans =

22.909760215804827


time =

3

可以看到,Aitken加速迭代法仅需3次迭代就得到了符合条件的解,且它的迭代误差只用$0.000000000176165$,小于上述所有的方法,由此该方法的优势得以体现。

Newton迭代法

Newton迭代法也是一种求解非线性方程的高效算法,因此我也对其进行实现。
这里要用到func的导数,经计算,编写func的导函数为程序df

1
2
3
4
5
6
function [y] = df(x)
%求导数
%统一使用弧度制
format long
y = (0.75 * sin(x) / (1 - (1 / (1.878 + 0.75 * cos(x))) ^ 2) ^ 0.5) / (1.878 + 0.75 * cos(x)) ^ 2 - 1;
end

Newton迭代法

编写Newton迭代法的计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function [errors, ans, time] = newton(x, max, stopError)
%Newton迭代法
%func是待求零点的函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
o = x - func(x) / df(x);
error = abs(o - x);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

输入命令[errorsN, ans, time] = newton(0, 50, 1.7e-5)进行计算,得解:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsN =

0.390355832559508 0.009488988787132 0.000005925259246


ans =

22.909760215672179


time =

3

Newton下山法

为尽可能避免因初值选取不当导致计算过程缓慢收敛或者发散(经上面计算,此问题不存在这种情况),引入下山因子$\lambda \in (0,1]$,得到改进后的Newton下山法,其计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function [errors, ans, time] = hill(x, max, stopError)
%Newton下山法
% 此处显示详细说明
format long %为提高精度,保留更多位数
l = 1;
for i = 1 : max
o = x - l * func(x) / df(x);
while abs(func(o)) > abs(func(x)) %不满足下山条件
l = l / 2;
o = x - l * func(x) / df(x);
end
error = abs(o - x);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

计算得到结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsH =

0.390355832559508 0.009488988787132 0.000005925259246


ans =

22.909760215672179


time =

3

由于此问题初值选取得当,即初值取$0$时使用一般Newton迭代法不存在缓慢收敛或者发散的问题,因此Newton下山法在这里并没有发挥作用。可以看到,Newton迭代法仅$3$步就完成了求解的任务,非常高效。


问题的解

以上方法都得到了一致的答案,根据精度要求,我们得出此条件下功率角$\theta$为$22.910^{\circ}$,与许实章编《电机学习题集》中的例题答案$22.9^{\circ}$相吻合。


方法比较

将上述各种方法的误差记录绘制成图表,由于Newton下山法在该问题中没有发挥作用,因此仅作出Newton迭代法的迭代误差图像。

根据图片,我们可以观察到以上各个方法均收敛。其中,表现较为优越的有Aitken加速迭代法、Newton迭代法和Steffensen迭代法,均用了$3$次迭代就达到了精度要求。然而这里Steffensen法的收敛速度更依赖于初值的选取,当初值选为$0$时,它的收敛速度就类似于弦截法在该问题上的收敛速度了。因此,总的来说,对于这个问题,最高效的算法是Aitken加速迭代法和Newton迭代法。另外,二分法最简单却也最低效,迭代了$15$次才达到预设的精度要求。可见,各种算法的效率大致上与它们的复杂度和高级程度成正比关系。


碰到底线咯 后面没有啦

本文标题:matlab笔记:一个非线性方程问题的多种求解方法

文章作者:高深远

发布时间:2019年11月16日 - 00:35

最后更新:2020年02月07日 - 17:31

原始链接:https://gsy00517.github.io/matlab20191116003545/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:一个非线性方程问题的多种求解方法 | 高深远的博客

matlab笔记:一个非线性方程问题的多种求解方法

上周科学计算引论结课了,借着写上机报告的机会,我把书本上所有的求解非线性方程的方法(标量型)都用matlab实现了一下,并对一个实际工程问题进行求解。关于实现过程中遇到的问题以及注意事项,我已写在matlab笔记:久未使用之后踩的一堆坑内。


实际问题

在电机学中,凸极同步发电机的功角特性可表示为:

式中,$P_{em}$表示发电机的电磁功率;$E_{0}$表示发电机电势;$V$表示发电机端电压;$x_{q}$表示横轴同步电抗;$x_{d}$表示纵轴同步电抗;$\theta$表示功率角,$\theta \in \left ( 0,\frac{\pi }{2} \right )$。
如令$\frac{E_{0}V}{x_{d}}=P_{j}$,$V^{2}\left ( \frac{1}{x_{q}}-\frac{1}{x_{d}} \right )=P_{2e}$,则上式可以简化为:

在电力系统稳定计算中,我们往往要由上式求出功率角$\theta$。我们可以使用几何方法求解,也可以利用迭代法求解该非线性方程。
我们将上式变为:

以许实章编《电机学习题集》第367页26-1为例,将$P_{em}=1$,$P_{j}=1.878$,$P_{2e}=0.75$代入,得到方程:


问题求解

在《计算方法》第二章,我们学习了一些非线性方程的数值解法,这里我们分别使用几何法和迭代法求解上述问题并进行比较。设方程求解的预定精度为$0.001^{\circ}$即$1.7\times 10^{-3}rad$,由闭区间上连续函数的性质和初步估计,可确定方程的解位于区间$[0,\frac{\pi }{6}]$。

几何方法

由几何法的求解方法,我们定义函数:

求解$\theta$即求解$f(\theta )$在$[0,\frac{\pi }{6}]$上的零点。
在matlab中,将该函数实现如下:

1
2
3
4
5
6
function [y] = func(x)
%几何法函数方程
%统一使用弧度制
format long %为提高精度,保留更多位数
y = asin(1 / (1.878 + 0.75 * cos(x))) - x;
end

由于matlab计算过程中默认保留$4$位小数,为了提高精度,我使用了format long保留更多有效数字。值得注意的是,计算时统一使用弧度制,待求出解之后再使用rad2deg函数转化为角度。

二分法

根据二分法的格式,编写二分法函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function [errors, ans, time] = bisection(low, high, max, stopError) 
%二分法
%func是待求零点的函数
%low,high分别是解区间的上下限
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
fl = func(low);
fh = func(high);
error = high - low;
for i = 1 : max
mid = (low + high ) / 2;
fm = func(mid);
if fm * fl > 0
low = mid;
else high = mid;
end
error = high - low;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(mid); %转换成角度
end

输入命令[errorsB, ans, time] = bisection(0, pi / 6, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
errorsB =

列 1 至 5

0.261799387799149 0.130899693899575 0.065449846949787 0.032724923474894 0.016362461737447

列 6 至 10

0.008181230868723 0.004090615434362 0.002045307717181 0.001022653858590 0.000511326929295

列 11 至 15

0.000255663464648 0.000127831732324 0.000063915866162 0.000031957933081 0.000015978966540


ans =

22.909240722656250


time =

15

使用二分法共迭代$15$次,求得结果为$22.909240722656250^{\circ}$。此外,迭代误差为$0.000015978966540$,符合预设精度要求。然而,此种方法计算次数较多,因此我又尝试了下面的方法。

弦截法

根据弦截法的计算方法,编写弦截法函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function [errors, ans, time] = linecut(a, b, max, stopError)
%弦截法
%func是待求零点的函数
%a,b是在解的领域取的两点,这里取解区间的两个端点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
fa = func(a);
fb = func(b);
error = abs(a - b);
for i = 1 : max
x = b - (b - a) * fb / (fb - fa);
a = b;
b = x;
fa = func(a);
fb = func(b);
error = abs(a - b);
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(b); %转换成角度
end

输入命令[errorsL, ans, time] = linecut(0, pi / 6, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsL =

0.120609794441825 0.003164447033051 0.000026217912123 0.000000005427336


ans =

22.909760215805360


time =

4

与上面的二分法相比,弦截法只需计算$4$次,效率大大提高。计算所得结果为$22.909760215805360^{\circ}$,迭代误差为$0.000000005427336$,符合预设精度要求,而且比二分法的最终迭代误差更小,显然可以发现弦截法的收敛速度要快于二分法。下面我再用弦截法的改造方法Steffensen方法进行试验。

Steffensen方法

根据Steffensen方法的计算方法,编写函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function [errors, ans, time] = steffensen(x, max, stopError)
%Steffensen方法
%func是待求零点的函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
f = func(x);
for i = 1 : max
o = x - f ^ 2 / (f - func(x - f));
error = abs(x - o);
x = o;
f = func(o);
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(o); %转换成角度
end

选取初始点为$0$,输入命令[errorsS1, ans, time] = steffensen(0, 50, 1.7e-5),求得结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsS1 =

0.381516143583955 0.018291709371805 0.000042893415627 0.000000000236814


ans =

22.909760215804820


time =

4

发现计算次数没有比弦截法少,因此修改初值为$\frac{\pi }{6}$,再次计算,得结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsS2 =

0.125855705283236 0.002107105082521 0.000000571210576


ans =

22.909760215802425


time =

3

一般而言,Steffensen方法的收敛速度要快于弦截法,但这与初始点的选取有关。对于这个问题,当设初始点为$0$时,Steffensen方法的迭代次数与弦截法持平;当设初始点为$\frac{\pi }{6}$时,迭代次数才小于弦截法。因此,想让Steffensen方法更快地收敛,需选取合适的初始点。在我看来,Steffensen方法的一大优势就是它是一种单步迭代方法,相比二步迭代方法的弦截法,Steffensen方法只需要一个初值就可以开始迭代。

Picard迭代法

上述的方法都是基于几何图形的求解方法,而下面的Picard迭代法则是基于不动点原理给出的。
首先,我们编写迭代函数:

1
2
3
4
5
6
function [y] = interation(x)
%迭代函数
%统一使用弧度制
format long %为提高精度,保留更多位数
y = asin(1 / (1.878 + 0.75 * cos(x)));
end

我们使用如下命令查看函数图像:

1
2
3
>> x = 0 : pi / 36 : pi / 6;
>> y = asin(1 * (1.878 + 0.75 * cos(x)) .^ (-1));
>> plot(x, y)

得到:

易验证,该函数在$[0,\frac{\pi }{6}]$上满足Picard迭代条件。

Picard迭代法

根据Picard迭代法原理,定义Picard迭代函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function [errors, ans, time] = picard(x, max, stopError)
%Picard迭代法
%interation是迭代函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
o = interation(x);
error = abs(x - o);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(o); %转换成角度
end

输入命令[errorsP, ans, time] = picard(0, 50, 1.7e-5)求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsP =

0.390355832559508 0.009044503640668 0.000428788831489 0.000020583068808 0.000000988625736


ans =

22.909757357777192


time =

5

Picard迭代结果为$22.909757357777192^{\circ}$,且精度符合要求。下面使用Picard迭代法的改进Aitken加速迭代法进行试验。

Aitken加速迭代法

编写Aitken加速迭代法函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function [errors, ans, time] = aitken(x, max, stopError)
%Aitken迭代法
%interation是迭代函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
x1 = interation(x);
x2 = interation(x1);
x3 = x2 - (x2 - x1) ^ 2 / (x2 - 2 * x1 + x);
error = abs(x3 - x);
x = x3;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

输入命令[errorsA, ans, time] = aitken(0, 50, 1.7e-5)求解得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsA =

0.399614867056990 0.000235879375045 0.000000000176165


ans =

22.909760215804827


time =

3

可以看到,Aitken加速迭代法仅需3次迭代就得到了符合条件的解,且它的迭代误差只用$0.000000000176165$,小于上述所有的方法,由此该方法的优势得以体现。

Newton迭代法

Newton迭代法也是一种求解非线性方程的高效算法,因此我也对其进行实现。
这里要用到func的导数,经计算,编写func的导函数为程序df

1
2
3
4
5
6
function [y] = df(x)
%求导数
%统一使用弧度制
format long
y = (0.75 * sin(x) / (1 - (1 / (1.878 + 0.75 * cos(x))) ^ 2) ^ 0.5) / (1.878 + 0.75 * cos(x)) ^ 2 - 1;
end

Newton迭代法

编写Newton迭代法的计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function [errors, ans, time] = newton(x, max, stopError)
%Newton迭代法
%func是待求零点的函数
%x是初始点
%max是最多循环步数,防止死循环
%stopError是预定精度,作为终止条件
%errors记录每次循环的误差
%ans记录最终求解结果,表示为角度
%time是总的循环次数
format long %为提高精度,保留更多位数
for i = 1 : max
o = x - func(x) / df(x);
error = abs(o - x);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

输入命令[errorsN, ans, time] = newton(0, 50, 1.7e-5)进行计算,得解:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsN =

0.390355832559508 0.009488988787132 0.000005925259246


ans =

22.909760215672179


time =

3

Newton下山法

为尽可能避免因初值选取不当导致计算过程缓慢收敛或者发散(经上面计算,此问题不存在这种情况),引入下山因子$\lambda \in (0,1]$,得到改进后的Newton下山法,其计算程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function [errors, ans, time] = hill(x, max, stopError)
%Newton下山法
% 此处显示详细说明
format long %为提高精度,保留更多位数
l = 1;
for i = 1 : max
o = x - l * func(x) / df(x);
while abs(func(o)) > abs(func(x)) %不满足下山条件
l = l / 2;
o = x - l * func(x) / df(x);
end
error = abs(o - x);
x = o;
errors(i) = error;
if error < stopError, break, end
end
time = i;
ans = rad2deg(x); %转换成角度
end

计算得到结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
errorsH =

0.390355832559508 0.009488988787132 0.000005925259246


ans =

22.909760215672179


time =

3

由于此问题初值选取得当,即初值取$0$时使用一般Newton迭代法不存在缓慢收敛或者发散的问题,因此Newton下山法在这里并没有发挥作用。可以看到,Newton迭代法仅$3$步就完成了求解的任务,非常高效。


问题的解

以上方法都得到了一致的答案,根据精度要求,我们得出此条件下功率角$\theta$为$22.910^{\circ}$,与许实章编《电机学习题集》中的例题答案$22.9^{\circ}$相吻合。


方法比较

将上述各种方法的误差记录绘制成图表,由于Newton下山法在该问题中没有发挥作用,因此仅作出Newton迭代法的迭代误差图像。

根据图片,我们可以观察到以上各个方法均收敛。其中,表现较为优越的有Aitken加速迭代法、Newton迭代法和Steffensen迭代法,均用了$3$次迭代就达到了精度要求。然而这里Steffensen法的收敛速度更依赖于初值的选取,当初值选为$0$时,它的收敛速度就类似于弦截法在该问题上的收敛速度了。因此,总的来说,对于这个问题,最高效的算法是Aitken加速迭代法和Newton迭代法。另外,二分法最简单却也最低效,迭代了$15$次才达到预设的精度要求。可见,各种算法的效率大致上与它们的复杂度和高级程度成正比关系。


碰到底线咯 后面没有啦

本文标题:matlab笔记:一个非线性方程问题的多种求解方法

文章作者:高深远

发布时间:2019年11月16日 - 00:35

最后更新:2020年02月07日 - 17:31

原始链接:https://gsy00517.github.io/matlab20191116003545/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:安装MinGW编译器 | 高深远的博客

matlab笔记:安装MinGW编译器

因为目标追踪领域最著名的比赛VOT(Visual Object Tracking),同时也拥有一个非常重要的数据集和一套比较权威的评价指标,基于的是matlab,因此我又开始用起了matlab(这么看下来貌似matlab要成我本学期用的最多的语言了)。当我download官方的toolkit之后,按着document一路比较顺利地操作了下来,结果突然遇到一个报错,说我没有C和C++编译器。WTF?我用了这么久居然都没发现这个问题…
本以为按照提示就能很快解决,结果这个问题折腾了我一整个晚上。好吧既然被折磨得这么惨那我还是本着逢血泪必写博的原则在这里写一下吧。
不过不得不说,最后成功的时候真的还是挺爽的哈哈!

References

电子文献:
https://ww2.mathworks.cn/help/matlab/matlab_external/compiling-c-mex-files-with-mingw.html?requestedDomain=uk.mathworks.com
https://ww2.mathworks.cn/matlabcentral/fileexchange/52848-matlab-support-for-mingw-w64-c-c-compiler
https://www.cnblogs.com/Vae1990Silence/p/10102375.html
https://blog.csdn.net/fly910905/article/details/86222946


MinGW

MinGW,是Minimalist GNU for Windows的缩写。它是一个可自由使用和自由发布的Windows特定头文件和使用GNU工具集导入库的集合,允许你在GNU/Linux和Windows平台生成本地的Windows程序而不需要第三方C运行时(C Runtime)库。
当初报错的时候,我也是很诧异,因为之前使用CodeBlocks和Visual Studio的时候明明是有的。而这次在matlab中编译C/C++时怎么就找不到了。
因为之前使用CodeBlocks也有找不到的情况,我当时是重装了一遍CodeBlocks解决问题的,因此我上网开了一下有没有类似的方法。按道理来说已经装了VS是可以找到的,但好像也存在即使安装了VS、matlab还是找不到编译器的情况。可以使用mex看一下具体是哪些路径没有匹配上,似乎可以通过修改注册表的方法解决,但我没有尝试。

注:matlab调用C/C++的方式主要有两种:利用MEX技术和调用C/C++动态连接库。MEX是Matlab Executable的缩写,它是一种“可在matlab中调用的C(或Fortran)语言衍生程序”。后文中还会用到。


失败的方法

毕竟是花了一个晚上,看了mathworks上网友们的各种solution,试错了许多方法,这里记录两个貌似要成功的方法(其实最后还是失败了),或许会有参考价值。
这里有一个被许多网友强调、要注意的是:下载后的mingw文件(没错它只有15kb看上去好假)要在打开的matlab中,找到相应的下载目录,右键点击然后选择下载并安装(download and install),否则似乎会出错。

  1. 禁用IPv6

    IPv6,顾名思义,就是IP地址的第6版协议。
    我们现在用的是IPv4,它的地址是32位,总数有43亿个左右,还要减去内网专用的192、170地址段,这样一来就更少了。
    然而,IPv6的地址是128位的,大概是43亿的4次方,地址极为丰富。
    网友Kshitij Mall给出了这样一个解决思路:

    1. 首先打开控制面板中的编辑系统环境变量。
    2. 在高级选项中,点击环境变量。
    3. 在系统变量栏中添加如下两个变量:(1)variable name:“JAVA_TOOL_OPTIONS”;value:“-Djava.net.preferIPv4Stack=true”;(2)variable name:“JAVA_OPTIONS”;value:“-Djava.net.preferIPv4Stack=true”。另外根据该网友所述,安装完成后可以删去这两个环境变量。

      注意:仅输入引号内部的内容。

      java文档指示,设置jvm属性java.net.preferIPv4Stack为true时,就可以禁用IPv6。反之,若设为0,则启用。

      注:禁用设置时不需要重启系统。

      由于该网友的系统配置和我相同(MATLAB 2019a on Windows 10 system with 64 bits),于是我毫不犹豫地首先尝试了他的方法,不知道是卡进去的原因还是为何,我成功看到了协议界面(原本一直卡在附加功能管理器加载的空白界面),但是最终开始下载支持包失败。

  2. 关闭防火墙

    失败之后,我继续看评论,看到一个网友感谢另一个网友提供的solution,我非常激动,感觉自己也要跟着解决了。
    似乎好像取得了一定的进展,但是在下载第三方包的时候还是卡住了(3rt party download error),我浏览了大多数网友的评论,貌似大家基本上都卡在了这里。

成功的方法

当我快要绝望的时候,突然看到了评论区感谢三连,都是感谢同一个人的。这已经是2018年的一个回复了,看评论发现他们使用的是Windows 7系统,但我决定还是试一下,结果真的成功了,非常感谢最初的solution提供者pawan singh!
具体方法如下:

  1. 这个网址下载合适的TDM-GCC。
  2. 下载之后,create一个新的到设定的安装路径中。

    注意:根据matlab文档(文首第一个参考链接)。MinGW的安装文件夹名称不能包含空格。例如,不要使用:C:\Program Files\mingw-64。应改用:C:\mingw-64。我建议直接装在C盘下面,默认似乎也是这样,维持不变即可。

  3. 与之前修改系统变量方式类似。添加新的系统变量名为MW_MINGW64_LOC,值为MinGW-w64编译器的安装位置,于我是C:\TDM-GCC-64。最后别忘了确定设置。
  4. 在matlab命令行内执行命令:setenv('MW_MINGW64_LOC', 'path'),folder为TDM-GCC的安装位置,要加单引号。例如我是:setenv('MW_MINGW64_LOC', 'C:\TDM-GCC-64')
  5. 可以继续在命令行中执行命令:mex -setup。若没有报错,则表明成功了。

然而

英语老师说,however后面的往往是重点,那么我这里就however一下。
上面的方法问题是没有,但是使用的时候有可能会收到警告:使用的是不受支持的MinGW编译器版本。
如果没有收到这个警告,那么就万事大吉,如果有的话,能运行的话依旧还是万事大吉。
But如果真的因此而运行出错,或者看着warning心里实在不舒服的话,可以看一下我后来写的matlab笔记:MEX文件函数使用中的问题


碰到底线咯 后面没有啦

本文标题:matlab笔记:安装MinGW编译器

文章作者:高深远

发布时间:2020年01月15日 - 22:26

最后更新:2020年01月21日 - 19:59

原始链接:https://gsy00517.github.io/matlab20200115222641/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:安装MinGW编译器 | 高深远的博客

matlab笔记:安装MinGW编译器

因为目标追踪领域最著名的比赛VOT(Visual Object Tracking),同时也拥有一个非常重要的数据集和一套比较权威的评价指标,基于的是matlab,因此我又开始用起了matlab(这么看下来貌似matlab要成我本学期用的最多的语言了)。当我download官方的toolkit之后,按着document一路比较顺利地操作了下来,结果突然遇到一个报错,说我没有C和C++编译器。WTF?我用了这么久居然都没发现这个问题…
本以为按照提示就能很快解决,结果这个问题折腾了我一整个晚上。好吧既然被折磨得这么惨那我还是本着逢血泪必写博的原则在这里写一下吧。
不过不得不说,最后成功的时候真的还是挺爽的哈哈!

References

电子文献:
https://ww2.mathworks.cn/help/matlab/matlab_external/compiling-c-mex-files-with-mingw.html?requestedDomain=uk.mathworks.com
https://ww2.mathworks.cn/matlabcentral/fileexchange/52848-matlab-support-for-mingw-w64-c-c-compiler
https://www.cnblogs.com/Vae1990Silence/p/10102375.html
https://blog.csdn.net/fly910905/article/details/86222946


MinGW

MinGW,是Minimalist GNU for Windows的缩写。它是一个可自由使用和自由发布的Windows特定头文件和使用GNU工具集导入库的集合,允许你在GNU/Linux和Windows平台生成本地的Windows程序而不需要第三方C运行时(C Runtime)库。
当初报错的时候,我也是很诧异,因为之前使用CodeBlocks和Visual Studio的时候明明是有的。而这次在matlab中编译C/C++时怎么就找不到了。
因为之前使用CodeBlocks也有找不到的情况,我当时是重装了一遍CodeBlocks解决问题的,因此我上网开了一下有没有类似的方法。按道理来说已经装了VS是可以找到的,但好像也存在即使安装了VS、matlab还是找不到编译器的情况。可以使用mex看一下具体是哪些路径没有匹配上,似乎可以通过修改注册表的方法解决,但我没有尝试。

注:matlab调用C/C++的方式主要有两种:利用MEX技术和调用C/C++动态连接库。MEX是Matlab Executable的缩写,它是一种“可在matlab中调用的C(或Fortran)语言衍生程序”。后文中还会用到。


失败的方法

毕竟是花了一个晚上,看了mathworks上网友们的各种solution,试错了许多方法,这里记录两个貌似要成功的方法(其实最后还是失败了),或许会有参考价值。
这里有一个被许多网友强调、要注意的是:下载后的mingw文件(没错它只有15kb看上去好假)要在打开的matlab中,找到相应的下载目录,右键点击然后选择下载并安装(download and install),否则似乎会出错。

  1. 禁用IPv6

    IPv6,顾名思义,就是IP地址的第6版协议。
    我们现在用的是IPv4,它的地址是32位,总数有43亿个左右,还要减去内网专用的192、170地址段,这样一来就更少了。
    然而,IPv6的地址是128位的,大概是43亿的4次方,地址极为丰富。
    网友Kshitij Mall给出了这样一个解决思路:

    1. 首先打开控制面板中的编辑系统环境变量。
    2. 在高级选项中,点击环境变量。
    3. 在系统变量栏中添加如下两个变量:(1)variable name:“JAVA_TOOL_OPTIONS”;value:“-Djava.net.preferIPv4Stack=true”;(2)variable name:“JAVA_OPTIONS”;value:“-Djava.net.preferIPv4Stack=true”。另外根据该网友所述,安装完成后可以删去这两个环境变量。

      注意:仅输入引号内部的内容。

      java文档指示,设置jvm属性java.net.preferIPv4Stack为true时,就可以禁用IPv6。反之,若设为0,则启用。

      注:禁用设置时不需要重启系统。

      由于该网友的系统配置和我相同(MATLAB 2019a on Windows 10 system with 64 bits),于是我毫不犹豫地首先尝试了他的方法,不知道是卡进去的原因还是为何,我成功看到了协议界面(原本一直卡在附加功能管理器加载的空白界面),但是最终开始下载支持包失败。

  2. 关闭防火墙

    失败之后,我继续看评论,看到一个网友感谢另一个网友提供的solution,我非常激动,感觉自己也要跟着解决了。
    似乎好像取得了一定的进展,但是在下载第三方包的时候还是卡住了(3rt party download error),我浏览了大多数网友的评论,貌似大家基本上都卡在了这里。

成功的方法

当我快要绝望的时候,突然看到了评论区感谢三连,都是感谢同一个人的。这已经是2018年的一个回复了,看评论发现他们使用的是Windows 7系统,但我决定还是试一下,结果真的成功了,非常感谢最初的solution提供者pawan singh!
具体方法如下:

  1. 这个网址下载合适的TDM-GCC。
  2. 下载之后,create一个新的到设定的安装路径中。

    注意:根据matlab文档(文首第一个参考链接)。MinGW的安装文件夹名称不能包含空格。例如,不要使用:C:\Program Files\mingw-64。应改用:C:\mingw-64。我建议直接装在C盘下面,默认似乎也是这样,维持不变即可。

  3. 与之前修改系统变量方式类似。添加新的系统变量名为MW_MINGW64_LOC,值为MinGW-w64编译器的安装位置,于我是C:\TDM-GCC-64。最后别忘了确定设置。
  4. 在matlab命令行内执行命令:setenv('MW_MINGW64_LOC', 'path'),folder为TDM-GCC的安装位置,要加单引号。例如我是:setenv('MW_MINGW64_LOC', 'C:\TDM-GCC-64')
  5. 可以继续在命令行中执行命令:mex -setup。若没有报错,则表明成功了。

然而

英语老师说,however后面的往往是重点,那么我这里就however一下。
上面的方法问题是没有,但是使用的时候有可能会收到警告:使用的是不受支持的MinGW编译器版本。
如果没有收到这个警告,那么就万事大吉,如果有的话,能运行的话依旧还是万事大吉。
But如果真的因此而运行出错,或者看着warning心里实在不舒服的话,可以看一下我后来写的matlab笔记:MEX文件函数使用中的问题


碰到底线咯 后面没有啦

本文标题:matlab笔记:安装MinGW编译器

文章作者:高深远

发布时间:2020年01月15日 - 22:26

最后更新:2020年01月21日 - 19:59

原始链接:https://gsy00517.github.io/matlab20200115222641/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:MEX文件函数使用中的问题 | 高深远的博客

matlab笔记:MEX文件函数使用中的问题

之前在matlab笔记:安装MinGW编译器一文中已经介绍过,MEX文件函数是Matlab提供的一种混合编程方式。通过MEX,用户可以在matlab中调用C、C++(没有C#,但我想提一下其实C#的真正含义是C++++,因为#其实就是四个+)或者Fortran编写的计算程序,加速matlab内部的矩阵运算(尤其是加速matlab代码中的for循环)。mex本质上是一个动态链接库文件(dll),可以被matlab动态加载并执行。然而在使用的过程中,我又碰到了许多问题。

References

电子文献:
https://ww2.mathworks.cn/help/matlab/call-mex-file-functions.html
https://blog.csdn.net/hijack00/article/details/52228253
https://jingyan.baidu.com/article/3a2f7c2ea00a9c66aed61163.html


安装版本适配的MinGW编译器

根据之前文章中写的配置方法,在编译MEX的时候,虽然没有问题,但是却出现了警告:使用的是不受支持的MinGW编译器版本。
于是我先查看了当前使用的编译器的版本。方法如下:

  1. 在MinGW-w64编译器的安装目录中,找到gcc.exe可执行文件的存在位置。
  2. 打开命令行,切换到刚刚找到的gcc.exe文件所在的目录。
  3. 键入gcc -v即可查看当前编译器的版本。

这时我使用的是5.1.0版本,于是我又到mathworks的网站上看了一下各个matlab版本适配的编译器版本。

这里我把图片截过来了,就不用去找了。我使用的是matlab R2019a,因此适配的是MinGW GCC 6.3(似乎高一点或者低一点都不行)。
于是我根据它所提供的SourceForge网址去找新版本的安装包,下载解压之后是一个不含任何可执行文件(exe)的文件夹,而且是7.0.0版本的,无法自主地选择。
寻找良久之后,我终于发现了一个在线安装文件,也建议下载这个,因为后面需要选择特定版本来安装。

下载完成后直接双击安装,这里会有一个安装设置界面。这个要注意一下,别点过去了。版本号一定要设置成对应的,比如我是6.3.0;另外,由于安装在windows 64位系统上,所以选择x86_64以及win32;至于其它的选项可以任选,一般默认就好了。

之后就是一路“下一步”,记得记住安装路径。
之后就是用和matlab笔记:安装MinGW编译器中所写的相同的方式添加环境变量。可以直接把之前已有的MW_MINGW64_LOC的值替换成刚刚记下的路径,最后别忘了在matlab中setenv
这时也可以把之前的编译器删了,如果是TDM-GCC的话那很方便,直接在它的一个管理界面中uninstall就行了,另外还会剩下一个空文件夹,手动删除就行。


连接外部库

在我使用的过程中,我还遇到了如下ERROR: Unable to compile MEX function: “MEX 找不到使用 -l 选项指定的库 ‘ut’。

上网搜了一下后,我才知道MEX命令可以用-L选项指定第三方库的路径,用-l来连接第三方库文件。值得注意的是,这里使用的是该库文件的文件名,不包含其文件扩展名。其基本格式如下:

1
mex -L<library_path> -l<library>

可以发现,-L<library_path>-l<library>之间是没有加空格的。
可是看了这些,我还是解决不了我的问题。
这里说一下我出现这个问题的背景,最近接触计算机视觉中的目标跟踪这一块,正在学习vot-toolkit的使用。
我既问了度娘又问了谷哥,可是没有看到任何这个问题及其解决方法。于是我缩小范围,看了看Github上vot-toolkit的issues和VOT Challenge technical support的Google groups,惊喜的是的确都找到了同样的问题。


然而都只有问题没有解答。无奈,还是自己想办法吧。
其实跟着报错的提示来修改并不难,关键是要找到该修改哪里。
由于报错提示的函数中根本没有MEX连接库文件的指令(我一行一行代码找的),于是我想能不能找到MEX的编译文件。最终我在vot-toolkit-master\utilities中找到了一个名为compile_mex.m的matlab文件,其中有这样的一串代码。

1
2
3
4
5
if is_octave()
arguments{end+1} = '-DOCTAVE';
else
arguments{end+1} = '-lut';
end

我用的是matlab,不是octave(后者相当于轻量级的免费matlab,语法什么的基本一致),那么执行的应该是else后面的语句,而在这里可以看到调用ut的命令。
于是我就把这里的-lut改成了-Lut试了一下,果然成功了。
其实用有搜索功能的IDE的话或许能够更快地解决这个问题。


碰到底线咯 后面没有啦

本文标题:matlab笔记:MEX文件函数使用中的问题

文章作者:高深远

发布时间:2020年01月21日 - 19:47

最后更新:2020年01月28日 - 09:56

原始链接:https://gsy00517.github.io/matlab20200121194751/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:MEX文件函数使用中的问题 | 高深远的博客

matlab笔记:MEX文件函数使用中的问题

之前在matlab笔记:安装MinGW编译器一文中已经介绍过,MEX文件函数是Matlab提供的一种混合编程方式。通过MEX,用户可以在matlab中调用C、C++(没有C#,但我想提一下其实C#的真正含义是C++++,因为#其实就是四个+)或者Fortran编写的计算程序,加速matlab内部的矩阵运算(尤其是加速matlab代码中的for循环)。mex本质上是一个动态链接库文件(dll),可以被matlab动态加载并执行。然而在使用的过程中,我又碰到了许多问题。

References

电子文献:
https://ww2.mathworks.cn/help/matlab/call-mex-file-functions.html
https://blog.csdn.net/hijack00/article/details/52228253
https://jingyan.baidu.com/article/3a2f7c2ea00a9c66aed61163.html


安装版本适配的MinGW编译器

根据之前文章中写的配置方法,在编译MEX的时候,虽然没有问题,但是却出现了警告:使用的是不受支持的MinGW编译器版本。
于是我先查看了当前使用的编译器的版本。方法如下:

  1. 在MinGW-w64编译器的安装目录中,找到gcc.exe可执行文件的存在位置。
  2. 打开命令行,切换到刚刚找到的gcc.exe文件所在的目录。
  3. 键入gcc -v即可查看当前编译器的版本。

这时我使用的是5.1.0版本,于是我又到mathworks的网站上看了一下各个matlab版本适配的编译器版本。

这里我把图片截过来了,就不用去找了。我使用的是matlab R2019a,因此适配的是MinGW GCC 6.3(似乎高一点或者低一点都不行)。
于是我根据它所提供的SourceForge网址去找新版本的安装包,下载解压之后是一个不含任何可执行文件(exe)的文件夹,而且是7.0.0版本的,无法自主地选择。
寻找良久之后,我终于发现了一个在线安装文件,也建议下载这个,因为后面需要选择特定版本来安装。

下载完成后直接双击安装,这里会有一个安装设置界面。这个要注意一下,别点过去了。版本号一定要设置成对应的,比如我是6.3.0;另外,由于安装在windows 64位系统上,所以选择x86_64以及win32;至于其它的选项可以任选,一般默认就好了。

之后就是一路“下一步”,记得记住安装路径。
之后就是用和matlab笔记:安装MinGW编译器中所写的相同的方式添加环境变量。可以直接把之前已有的MW_MINGW64_LOC的值替换成刚刚记下的路径,最后别忘了在matlab中setenv
这时也可以把之前的编译器删了,如果是TDM-GCC的话那很方便,直接在它的一个管理界面中uninstall就行了,另外还会剩下一个空文件夹,手动删除就行。


连接外部库

在我使用的过程中,我还遇到了如下ERROR: Unable to compile MEX function: “MEX 找不到使用 -l 选项指定的库 ‘ut’。

上网搜了一下后,我才知道MEX命令可以用-L选项指定第三方库的路径,用-l来连接第三方库文件。值得注意的是,这里使用的是该库文件的文件名,不包含其文件扩展名。其基本格式如下:

1
mex -L<library_path> -l<library>

可以发现,-L<library_path>-l<library>之间是没有加空格的。
可是看了这些,我还是解决不了我的问题。
这里说一下我出现这个问题的背景,最近接触计算机视觉中的目标跟踪这一块,正在学习vot-toolkit的使用。
我既问了度娘又问了谷哥,可是没有看到任何这个问题及其解决方法。于是我缩小范围,看了看Github上vot-toolkit的issues和VOT Challenge technical support的Google groups,惊喜的是的确都找到了同样的问题。


然而都只有问题没有解答。无奈,还是自己想办法吧。
其实跟着报错的提示来修改并不难,关键是要找到该修改哪里。
由于报错提示的函数中根本没有MEX连接库文件的指令(我一行一行代码找的),于是我想能不能找到MEX的编译文件。最终我在vot-toolkit-master\utilities中找到了一个名为compile_mex.m的matlab文件,其中有这样的一串代码。

1
2
3
4
5
if is_octave()
arguments{end+1} = '-DOCTAVE';
else
arguments{end+1} = '-lut';
end

我用的是matlab,不是octave(后者相当于轻量级的免费matlab,语法什么的基本一致),那么执行的应该是else后面的语句,而在这里可以看到调用ut的命令。
于是我就把这里的-lut改成了-Lut试了一下,果然成功了。
其实用有搜索功能的IDE的话或许能够更快地解决这个问题。


碰到底线咯 后面没有啦

本文标题:matlab笔记:MEX文件函数使用中的问题

文章作者:高深远

发布时间:2020年01月21日 - 19:47

最后更新:2020年01月28日 - 09:56

原始链接:https://gsy00517.github.io/matlab20200121194751/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:在服务器上激活matlab遇到的问题 | 高深远的博客

matlab笔记:在服务器上激活matlab遇到的问题

前段时间在服务器上使用matlab,在激活的时候遇到了一些和自己个人电脑上不一样的情况,在搞清楚之后记录一下。

References

电子文献:
https://ww2.mathworks.cn/matlabcentral/answers/99067-why-do-i-receive-license-manager-error-9


问题

前段时间在学校的ubuntu服务器上安装了matlab,按照自己之前的方法在/usr/local/MATLAB/version/bin中使用./activate_matlab.sh激活并使用matlab命令直接运行,却卡在MATLAB is selecting SOFTWARE OPENGL rendering之后报错License Manager Error -9
我们知道,matlab激活时指定的用户是需要和本机用户名一致的,由于之前服务器上有学姐安装过matlab,我觉得可能是激活了她的用户名导致我无法使用,因此我反激活之前的用户名之后,尝试使用root作为指定用户重新激活并使用sudo matlab命令运行matlab,结果却显示没有已激活的许可证。
后来想想的确不应该是这个问题,因为matlab每次激活会覆盖之前的,假如是因为别的用户名导致我无法运行的话,我激活后应该就能解决,然而我却依旧不能运行。
于是,我直接在bin中打开matlab的执行文件,即./matlab(如果之前用root激活的话则输入sudo ./matlab就能执行),打开是打开了,但打开过程比较慢且出现一长串报错。


解决办法

在搞清楚matlab激活的要求之后,我还是使用自己的用户名去激活matlab。当然,别的用户也可以使用自己的用户名激活,经检验,这样并不会影响大家共同的使用。
然而,此时仍旧只能在bin中打开matlab并且会有如上文图中所示的报错。搜索之后发现,是我没有把.matlab的整个目录设为可写。注意,这个目录指的是用户主文件夹下的目录而不是/usr/local下面的安装目录。
因此,只需赋予更多权限,就不会出现之前的问题了。可以在终端中执行如下语句。

1
sudo chmod -R 777 /home/***/.matlab

注意:这里的***是用户名,-R是为了对整个文件夹操作。


如何查看本机用户名

一般情况下,执行matlab激活程序时会自动给出你的用户名,这样激活之后就可以直接使用。然而,有时候用户名并没有给出而需要自己填,而有可能不确定自己该填啥,这时候可以就通过如下方式查询。

  1. 还是执行激活程序,先不选择使用Internet自动激活,而是选择“在不使用Internet的情况下手动激活”。
  2. 点击下一步之后,选择“我没有许可证文件,帮我执行后续步骤”。
  3. 再到下一步,就可以看到用户名了,直接复制。
  4. 返回到开始的地方,选择“使用Internet自动激活”,之后还是一路默认操作,在需要指定用户时把之前的用户名粘贴上即可。

碰到底线咯 后面没有啦

本文标题:matlab笔记:在服务器上激活matlab遇到的问题

文章作者:高深远

发布时间:2020年05月02日 - 22:51

最后更新:2020年05月02日 - 23:27

原始链接:https://gsy00517.github.io/matlab20200502225129/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
matlab笔记:在服务器上激活matlab遇到的问题 | 高深远的博客

matlab笔记:在服务器上激活matlab遇到的问题

前段时间在服务器上使用matlab,在激活的时候遇到了一些和自己个人电脑上不一样的情况,在搞清楚之后记录一下。

References

电子文献:
https://ww2.mathworks.cn/matlabcentral/answers/99067-why-do-i-receive-license-manager-error-9


问题

前段时间在学校的ubuntu服务器上安装了matlab,按照自己之前的方法在/usr/local/MATLAB/version/bin中使用./activate_matlab.sh激活并使用matlab命令直接运行,却卡在MATLAB is selecting SOFTWARE OPENGL rendering之后报错License Manager Error -9
我们知道,matlab激活时指定的用户是需要和本机用户名一致的,由于之前服务器上有学姐安装过matlab,我觉得可能是激活了她的用户名导致我无法使用,因此我反激活之前的用户名之后,尝试使用root作为指定用户重新激活并使用sudo matlab命令运行matlab,结果却显示没有已激活的许可证。
后来想想的确不应该是这个问题,因为matlab每次激活会覆盖之前的,假如是因为别的用户名导致我无法运行的话,我激活后应该就能解决,然而我却依旧不能运行。
于是,我直接在bin中打开matlab的执行文件,即./matlab(如果之前用root激活的话则输入sudo ./matlab就能执行),打开是打开了,但打开过程比较慢且出现一长串报错。


解决办法

在搞清楚matlab激活的要求之后,我还是使用自己的用户名去激活matlab。当然,别的用户也可以使用自己的用户名激活,经检验,这样并不会影响大家共同的使用。
然而,此时仍旧只能在bin中打开matlab并且会有如上文图中所示的报错。搜索之后发现,是我没有把.matlab的整个目录设为可写。注意,这个目录指的是用户主文件夹下的目录而不是/usr/local下面的安装目录。
因此,只需赋予更多权限,就不会出现之前的问题了。可以在终端中执行如下语句。

1
sudo chmod -R 777 /home/***/.matlab

注意:这里的***是用户名,-R是为了对整个文件夹操作。


如何查看本机用户名

一般情况下,执行matlab激活程序时会自动给出你的用户名,这样激活之后就可以直接使用。然而,有时候用户名并没有给出而需要自己填,而有可能不确定自己该填啥,这时候可以就通过如下方式查询。

  1. 还是执行激活程序,先不选择使用Internet自动激活,而是选择“在不使用Internet的情况下手动激活”。
  2. 点击下一步之后,选择“我没有许可证文件,帮我执行后续步骤”。
  3. 再到下一步,就可以看到用户名了,直接复制。
  4. 返回到开始的地方,选择“使用Internet自动激活”,之后还是一路默认操作,在需要指定用户时把之前的用户名粘贴上即可。

碰到底线咯 后面没有啦

本文标题:matlab笔记:在服务器上激活matlab遇到的问题

文章作者:高深远

发布时间:2020年05月02日 - 22:51

最后更新:2020年05月02日 - 23:27

原始链接:https://gsy00517.github.io/matlab20200502225129/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
这些文章最近被看 | 高深远的博客

这些文章最近被看

注:由于LeanCloud访问原因,有时此页会出现无法正确显示的问题,请稍后重新刷新查看。

0%
这些文章最近被看 | 高深远的博客

这些文章最近被看

注:由于LeanCloud访问原因,有时此页会出现无法正确显示的问题,请稍后重新刷新查看。

0%
opencv笔记:ubuntu安装opencv以及多版本共存 | 高深远的博客

opencv笔记:ubuntu安装opencv以及多版本共存

跑CV程序少不了opencv,然后最近实践的时候发现opencv似乎不是向后兼容的(opencv4.0跑不了opencv2.4的),于是还是记录一下,后期改也方便。

References

电子文献:
https://blog.csdn.net/new_delete_/article/details/84797041?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.jianshu.com/p/f646448da265
https://blog.csdn.net/learning_tortosie/article/details/80594399


安装流程

本节以opencv4.0为例,记录一下在ubuntu18.04上安装的流程。此方法经检验,同样适用于opencv2.4。

准备

cmake

首先要确保系统已经安装了cmake,没有的话请sudo apt-get install cmake或者源码编译最新版。

依赖库

1
sudo apt-get install build-essential libgtk2.0-dev libgtk-3-dev libavcodec-dev libavformat-dev libjpeg-dev libswscale-dev libtiff5-dev

支持python

1
2
3
4
5
6
7
8
#python3支持
sudo apt install python3-dev python3-numpy

#streamer支持
sudo apt install libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev

#可选的依赖
sudo apt install libpng-dev libopenexr-dev libtiff-dev libwebp-dev

下载源文件

可以到官网或者github上去下载源文件(官网版本选择在下方翻页,github上通过tag选择)。
推荐使用github下载,相对会快一些。

安装

解压源文件并进入。

1
2
unzip opencv-4.0.0.zip
cd opencv-4.0.0/

创建编译文件夹并进入。

1
2
mkdir build
cd build/

cmake

opencv最好装在/usr/local目录下,为了后期方便,我将不同版本的opencv再独立键一级目录。

1
cmake -D CMAKE_BUILD_TYPE=Release -D OPENCV_GENERATE_PKGCONFIG=YES -D CMAKE_INSTALL_PREFIX=/usr/local/opencv4 ..

这里我修改了安装路径为/usr/local/opencv4,可以自己修改到需要的位置,如果该命令中不加-D CMAKE_INSTALL_PREFIX=/usr/local/opencv4来指定安装目录,则默认各部分分别安装在/usr/local/目录的includebinlib3个文件夹下。

注意:
别忘了最后的..。(cmake .是运行cmake,cmake ..会生成Makefile、CMakeFiles等一些文件)
由于opencv4.0以上版本默认不使用pkg-config,因此需要通过-D OPENCV_GENERATE_PKGCONFIG=YES开启生成opencv4.pc文件(后面要用),支持pkg-config功能。如果是安装opencv4.0以下的软件,也加上这句是不会有影响的。
这个命令执行需要一段时间,期间会输出百分比,无需担心。

make编译

1
make -j6

这里-j6表示开6个线程去编译,-j2-j4或者不加其实任意。

make安装

1
sudo make install

以上就完成了安装,下面来配置环境。

配置环境

pkg-config环境

我们可以通过sudo find / -iname opencv4.pc来找到opencv4.pc文件,这里会出现一个Permission denied的报错,但是没关系,路径还是会输出。如果按照之前的方法安装,那么应该是会输出/usr/local/opencv4/lib/pkgconfig/opencv4.pc。因此我们接下来就要把路径/usr/local/opencv4/lib/pkgconfig/加入到PKG_CONFIG_PATH
可以用vim来方便地编辑。编辑方法我在anaconda笔记:解决conda无法下载pytorch的问题一文中已经提过,只需掌握insert模式和normal模式即可正确地操作vim。

1
sudo vim /etc/profile.d/pkgconfig.sh

在文件中加入下面一行:

1
export PKG_CONFIG_PATH=/usr/local/opencv4/lib/pkgconfig:$PKG_CONFIG_PATH

:wq保存退出之后,在终端中使用命令激活。

1
source /etc/profile

最后我们可以通过pkg-config --libs opencv4(opencv2.4使用pkg-config --libs opencv)来验证配置是否成功,如果能输出一系列对应的库,说明配置成功。

动态库环境

为了在程序执行时能加载动态库*.so的路径,我们还需配置动态库环境。

1
sudo vim /etc/ld.so.conf.d/opencv4.conf

在该文件(可能是空文件,也可能是/usr/local/lib),加上或者改成自己lib的安装路径,如果是按照上面的流程安装的,那应该是改成/usr/local/opencv4/lib
最后还要用命令使得配置生效。

1
sudo ldconfig

python-opencv环境

找到编译好的python cv库:

1
sudo find / -iname cv2*.so

这里会看到cv2.cpython-35m-x86_64-linux-gnu.so也就是就是编译好的python3的opencv库,我们把它复制到对应python解释器的/path/to/dist-packages(系统自带的python解释器)和/path/to/site-packages(用户安装的python解释器)目录下,之后就能在该python解释器中使用python-opencv库。
连接到系统自带的python3解释器中:

1
sudo ln -s /usr/local/opencv4/lib/python3.5/dist-packages/cv2/python-3.5/cv2.cpython-35m-x86_64-linux-gnu.so /usr/lib/python3/dist-packages/cv2.so

连接到Anaconda创建的虚拟环境python3解释器中:

1
sudo ln -s /usr/local/opencv4/lib/python3.5/dist-packages/cv2/python-3.5/cv2.cpython-35m-x86_64-linux-gnu.so ~/anaconda3/lib/python3.7/site-packages/cv2.so

这里的ln -s是建立软连接,想进一步了解可以看看ubuntu笔记:安装typora

检验

找到之前解压的源文件,在源文件/samples/cpp/example_cmake目录下(也可能是在源文件/samples/c/example_cmake目录下),我们可以通过官方提供的example来检验。
依次执行:

1
2
3
cmake .
make
./opencv_example

可以看到电脑打开了摄像头拍你自己,在左上角有一个Hello OpenCV,即表示配置成功。


多版本共存

上面已经安装了opencv4.0,下面以opencv2.4为例,介绍一下如何从opencv4.0切换到opencv2.4。
打开~/.bashrc

1
gedit ~/.bashrc

这里用gedit来打开,用vim也一样。
在文件末尾增加如下内容(我将opencv2.4安装在/usr/local/opencv2_4目录下):

1
2
export PKG_CONFIG_PATH=/usr/local/opencv2_4/lib/pkgconfig
export LD_LIBRARY_PATH=/usr/local/opencv2_4/lib

更新~/.bashrc

1
source ~/.bashrc

此时再通过pkg-config --modversion opencv查询opencv的版本,如输出修改后我们想要的版本,则表示切换成功。

注:如果想换回来,用同样的方法把之前加的两条增加的内容注释掉即可,注意别忘了source ~/.bashrc更新!


碰到底线咯 后面没有啦

本文标题:opencv笔记:ubuntu安装opencv以及多版本共存

文章作者:高深远

发布时间:2020年02月26日 - 09:23

最后更新:2020年02月26日 - 11:04

原始链接:https://gsy00517.github.io/opencv20200226092314/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
opencv笔记:ubuntu安装opencv以及多版本共存 | 高深远的博客

opencv笔记:ubuntu安装opencv以及多版本共存

跑CV程序少不了opencv,然后最近实践的时候发现opencv似乎不是向后兼容的(opencv4.0跑不了opencv2.4的),于是还是记录一下,后期改也方便。

References

电子文献:
https://blog.csdn.net/new_delete_/article/details/84797041?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.jianshu.com/p/f646448da265
https://blog.csdn.net/learning_tortosie/article/details/80594399


安装流程

本节以opencv4.0为例,记录一下在ubuntu18.04上安装的流程。此方法经检验,同样适用于opencv2.4。

准备

cmake

首先要确保系统已经安装了cmake,没有的话请sudo apt-get install cmake或者源码编译最新版。

依赖库

1
sudo apt-get install build-essential libgtk2.0-dev libgtk-3-dev libavcodec-dev libavformat-dev libjpeg-dev libswscale-dev libtiff5-dev

支持python

1
2
3
4
5
6
7
8
#python3支持
sudo apt install python3-dev python3-numpy

#streamer支持
sudo apt install libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev

#可选的依赖
sudo apt install libpng-dev libopenexr-dev libtiff-dev libwebp-dev

下载源文件

可以到官网或者github上去下载源文件(官网版本选择在下方翻页,github上通过tag选择)。
推荐使用github下载,相对会快一些。

安装

解压源文件并进入。

1
2
unzip opencv-4.0.0.zip
cd opencv-4.0.0/

创建编译文件夹并进入。

1
2
mkdir build
cd build/

cmake

opencv最好装在/usr/local目录下,为了后期方便,我将不同版本的opencv再独立键一级目录。

1
cmake -D CMAKE_BUILD_TYPE=Release -D OPENCV_GENERATE_PKGCONFIG=YES -D CMAKE_INSTALL_PREFIX=/usr/local/opencv4 ..

这里我修改了安装路径为/usr/local/opencv4,可以自己修改到需要的位置,如果该命令中不加-D CMAKE_INSTALL_PREFIX=/usr/local/opencv4来指定安装目录,则默认各部分分别安装在/usr/local/目录的includebinlib3个文件夹下。

注意:
别忘了最后的..。(cmake .是运行cmake,cmake ..会生成Makefile、CMakeFiles等一些文件)
由于opencv4.0以上版本默认不使用pkg-config,因此需要通过-D OPENCV_GENERATE_PKGCONFIG=YES开启生成opencv4.pc文件(后面要用),支持pkg-config功能。如果是安装opencv4.0以下的软件,也加上这句是不会有影响的。
这个命令执行需要一段时间,期间会输出百分比,无需担心。

make编译

1
make -j6

这里-j6表示开6个线程去编译,-j2-j4或者不加其实任意。

make安装

1
sudo make install

以上就完成了安装,下面来配置环境。

配置环境

pkg-config环境

我们可以通过sudo find / -iname opencv4.pc来找到opencv4.pc文件,这里会出现一个Permission denied的报错,但是没关系,路径还是会输出。如果按照之前的方法安装,那么应该是会输出/usr/local/opencv4/lib/pkgconfig/opencv4.pc。因此我们接下来就要把路径/usr/local/opencv4/lib/pkgconfig/加入到PKG_CONFIG_PATH
可以用vim来方便地编辑。编辑方法我在anaconda笔记:解决conda无法下载pytorch的问题一文中已经提过,只需掌握insert模式和normal模式即可正确地操作vim。

1
sudo vim /etc/profile.d/pkgconfig.sh

在文件中加入下面一行:

1
export PKG_CONFIG_PATH=/usr/local/opencv4/lib/pkgconfig:$PKG_CONFIG_PATH

:wq保存退出之后,在终端中使用命令激活。

1
source /etc/profile

最后我们可以通过pkg-config --libs opencv4(opencv2.4使用pkg-config --libs opencv)来验证配置是否成功,如果能输出一系列对应的库,说明配置成功。

动态库环境

为了在程序执行时能加载动态库*.so的路径,我们还需配置动态库环境。

1
sudo vim /etc/ld.so.conf.d/opencv4.conf

在该文件(可能是空文件,也可能是/usr/local/lib),加上或者改成自己lib的安装路径,如果是按照上面的流程安装的,那应该是改成/usr/local/opencv4/lib
最后还要用命令使得配置生效。

1
sudo ldconfig

python-opencv环境

找到编译好的python cv库:

1
sudo find / -iname cv2*.so

这里会看到cv2.cpython-35m-x86_64-linux-gnu.so也就是就是编译好的python3的opencv库,我们把它复制到对应python解释器的/path/to/dist-packages(系统自带的python解释器)和/path/to/site-packages(用户安装的python解释器)目录下,之后就能在该python解释器中使用python-opencv库。
连接到系统自带的python3解释器中:

1
sudo ln -s /usr/local/opencv4/lib/python3.5/dist-packages/cv2/python-3.5/cv2.cpython-35m-x86_64-linux-gnu.so /usr/lib/python3/dist-packages/cv2.so

连接到Anaconda创建的虚拟环境python3解释器中:

1
sudo ln -s /usr/local/opencv4/lib/python3.5/dist-packages/cv2/python-3.5/cv2.cpython-35m-x86_64-linux-gnu.so ~/anaconda3/lib/python3.7/site-packages/cv2.so

这里的ln -s是建立软连接,想进一步了解可以看看ubuntu笔记:安装typora

检验

找到之前解压的源文件,在源文件/samples/cpp/example_cmake目录下(也可能是在源文件/samples/c/example_cmake目录下),我们可以通过官方提供的example来检验。
依次执行:

1
2
3
cmake .
make
./opencv_example

可以看到电脑打开了摄像头拍你自己,在左上角有一个Hello OpenCV,即表示配置成功。


多版本共存

上面已经安装了opencv4.0,下面以opencv2.4为例,介绍一下如何从opencv4.0切换到opencv2.4。
打开~/.bashrc

1
gedit ~/.bashrc

这里用gedit来打开,用vim也一样。
在文件末尾增加如下内容(我将opencv2.4安装在/usr/local/opencv2_4目录下):

1
2
export PKG_CONFIG_PATH=/usr/local/opencv2_4/lib/pkgconfig
export LD_LIBRARY_PATH=/usr/local/opencv2_4/lib

更新~/.bashrc

1
source ~/.bashrc

此时再通过pkg-config --modversion opencv查询opencv的版本,如输出修改后我们想要的版本,则表示切换成功。

注:如果想换回来,用同样的方法把之前加的两条增加的内容注释掉即可,注意别忘了source ~/.bashrc更新!


碰到底线咯 后面没有啦

本文标题:opencv笔记:ubuntu安装opencv以及多版本共存

文章作者:高深远

发布时间:2020年02月26日 - 09:23

最后更新:2020年02月26日 - 11:04

原始链接:https://gsy00517.github.io/opencv20200226092314/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客高深远的博客paper笔记:文献中各种缩写汇总 | 高深远的博客

paper笔记:文献中各种缩写汇总

看paper的时候免不了会遇到一些似懂非懂的缩写,虽然基本上可以跳过,但是总是感觉会卡一下,这里就对我遇到过的做一个汇总,以后就可以哪里不会点哪里了。


拉丁文简写

e.g.

拉丁文exempli gratia,意为比如

etc.

拉丁文et cetera,意为等等

et al.

拉丁文et alia,意为等人

i.e.

拉丁文id est,意为也就是说;换句话说;即

viz.

拉丁文videlicet,意为即;就是

id.

拉丁文idem,意为同上

P.S.

拉丁文post scriptum,意为附言

cf.

拉丁文conf er,意为参看;比较


英文简写

w.r.t.

英文with respect to,意为关于,对于;谈及,谈到

eq.

英文equation,意为方程;等式。(有时也被写作Eqn.)

con.

英文contra,意为相反

const.

英文constant,意为常数;常量

fig.

英文figure,意为图形;数字

s.t.

英文such that,意为于是,因此;使得

st.

英文subject to,意为约束于

i.i.d.

英文independent and identically distributed,概率论术语,意为独立同分布


碰到底线咯 后面没有啦

本文标题:paper笔记:文献中各种缩写汇总

文章作者:高深远

发布时间:2020年02月06日 - 19:02

最后更新:2020年03月21日 - 17:31

原始链接:https://gsy00517.github.io/paper20200206190206/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
paper笔记:文献中各种缩写汇总 | 高深远的博客

paper笔记:文献中各种缩写汇总

看paper的时候免不了会遇到一些似懂非懂的缩写,虽然基本上可以跳过,但是总是感觉会卡一下,这里就对我遇到过的做一个汇总,以后就可以哪里不会点哪里了。


拉丁文简写

e.g.

拉丁文exempli gratia,意为比如

etc.

拉丁文et cetera,意为等等

et al.

拉丁文et alia,意为等人

i.e.

拉丁文id est,意为也就是说;换句话说;即

viz.

拉丁文videlicet,意为即;就是

id.

拉丁文idem,意为同上

P.S.

拉丁文post scriptum,意为附言

cf.

拉丁文conf er,意为参看;比较


英文简写

w.r.t.

英文with respect to,意为关于,对于;谈及,谈到

eq.

英文equation,意为方程;等式。(有时也被写作Eqn.)

con.

英文contra,意为相反

const.

英文constant,意为常数;常量

fig.

英文figure,意为图形;数字

s.t.

英文such that,意为于是,因此;使得

st.

英文subject to,意为约束于

i.i.d.

英文independent and identically distributed,概率论术语,意为独立同分布


碰到底线咯 后面没有啦

本文标题:paper笔记:文献中各种缩写汇总

文章作者:高深远

发布时间:2020年02月06日 - 19:02

最后更新:2020年03月21日 - 17:31

原始链接:https://gsy00517.github.io/paper20200206190206/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
欢迎到访:写在前面 | 高深远的博客

欢迎到访:写在前面

不知不觉,建站已有不少日子了,无论是内容还是界面,都逐渐丰富了起来。觉得有必要补充一篇类似于导言的文字,今天抽出点时间写一下,日后继续完善。


关于我

我来自浙江,就读于华科,目前是一名电子信息工程专业的大二本科生。从大学开始真正比较全面地接触信息技术,一年下来,在课余接触并尝试过的方面主要有编程语言python与R、linux操作系统、前端设计、机器学习与深度学习。我的博客也主要围绕这几个方面展开,具体也可以看看标签页,我也还在不断地探索与学习中。目前我主要学习的是计算机视觉方面的相关知识。
入门没多久,许多理解也还比较浅薄,博客内容主要是一些干货的搬运分享并结合自己积累的一些理解与经验,会有不足与疏漏,如果大家能给予指导,我将非常感激!今后我会尽量陆续加入更多深层次的内容。
对于这个博客网站,可以把它看作一个技术博客,而我更多的把它看成一个自己的空间,因此偶尔也会加入一些学习生活的元素,请别见怪!此外,我会尽我所能提升文章的质量,在发布后也会不断地查漏补缺,小幅修正与大幅补充结合,使每篇文章尽可能更规范易懂、更丰富充实。
或许在这个移动端主导的时代,搭网站写博客的价值有所降低。但当我写了一段时间之后,发现驱使我写博客的动力不减反增。最大的收获就是我每写一篇文章就会不断地想把相关的知识彻底搞懂,这就不断促使我去深究(哪怕钻牛角尖),因此总能收获好多写之前根本没想到的内容。


关于博客

我的文章在发布之后往往会做一些补充,此外写文章时往往把自己认为值得记录的都会写下来,因此有些文章的标题可能无法很好地概括文章的内容。可以试一试博客侧栏(手机模式下拉顶栏)的站内搜索,通过关键词或许能找到更多内容。
由于我的文章是按照最后一次更新时间早晚的顺序排列的,多了之后不方便查找和浏览,因此我新增了标签分类,或许可以帮助你更快地查看想看的内容。此外,也推荐使用我页面上的搜索功能利用关键词查找,非常便捷。

注意:PC体验更佳~

为了提高访问速度,我对我的博客进行了github+coding双线部署,url如下:
github page:https://gsy00517.github.io/
coding page:http://gsy00517.coding.me/
大家可以择优访问,事实上我并没有感到github的速度比coding慢。此外,由于我是同一个源文件双线部署,因此选择了把所有功能优先应用在github page上(比如文章打分、文章链接),但其实coding page也没有太大的区别。

注意:前期由于coding升级导致个人版page下线,好在可以迁移至新的coding,然而我尚未处理。不过github page可正常访问,也推荐访问我的github page。


关于订阅

首先,我想说的是,如果你觉得我写的内容或者方向对你有一点用处的话,非常欢迎收藏或订阅我的博客!如果你也在写博客的话,我们可以互相关注!
在侧栏,你可能会发现这样一个图标。点击之后,你会进入一个看不懂的atom.xml文件。

其实,看不懂是正常的,因为这个是给电脑看的。一个便捷的办法就是使用chrome的扩展程序添加feed(或者使用一些具有同样功能的手机app),然后打开网页时,就可以直接点击浏览器右上角的图标(会显示加号)进行订阅。这样以后每次更新了新的博文,你就可以收到提醒。


此外还可以像收藏其他网站一样进行收藏,这里不详述了。
欢迎大家常来踩踩,也欢迎大家留下评论。评论很简单,无需登录任何账号直接评论即可~
我目前也还在学习的过程中,欢迎大家和我交流,也欢迎各种批评与建议,我会努力改进!


Good News

好消息!好消息!本站已和百度、谷歌等世界知名搜索引擎达成战略合作关系!如果我写的文章有不足或疏漏之处、看完后有费解有困惑,都可以直接问度娘和谷哥就好啦~
哈哈哈皮这一下很开心。


碰到底线咯 后面没有啦
0%
欢迎到访:写在前面 | 高深远的博客

欢迎到访:写在前面

不知不觉,建站已有不少日子了,无论是内容还是界面,都逐渐丰富了起来。觉得有必要补充一篇类似于导言的文字,今天抽出点时间写一下,日后继续完善。


关于我

我来自浙江,就读于华科,目前是一名电子信息工程专业的大二本科生。从大学开始真正比较全面地接触信息技术,一年下来,在课余接触并尝试过的方面主要有编程语言python与R、linux操作系统、前端设计、机器学习与深度学习。我的博客也主要围绕这几个方面展开,具体也可以看看标签页,我也还在不断地探索与学习中。目前我主要学习的是计算机视觉方面的相关知识。
入门没多久,许多理解也还比较浅薄,博客内容主要是一些干货的搬运分享并结合自己积累的一些理解与经验,会有不足与疏漏,如果大家能给予指导,我将非常感激!今后我会尽量陆续加入更多深层次的内容。
对于这个博客网站,可以把它看作一个技术博客,而我更多的把它看成一个自己的空间,因此偶尔也会加入一些学习生活的元素,请别见怪!此外,我会尽我所能提升文章的质量,在发布后也会不断地查漏补缺,小幅修正与大幅补充结合,使每篇文章尽可能更规范易懂、更丰富充实。
或许在这个移动端主导的时代,搭网站写博客的价值有所降低。但当我写了一段时间之后,发现驱使我写博客的动力不减反增。最大的收获就是我每写一篇文章就会不断地想把相关的知识彻底搞懂,这就不断促使我去深究(哪怕钻牛角尖),因此总能收获好多写之前根本没想到的内容。


关于博客

我的文章在发布之后往往会做一些补充,此外写文章时往往把自己认为值得记录的都会写下来,因此有些文章的标题可能无法很好地概括文章的内容。可以试一试博客侧栏(手机模式下拉顶栏)的站内搜索,通过关键词或许能找到更多内容。
由于我的文章是按照最后一次更新时间早晚的顺序排列的,多了之后不方便查找和浏览,因此我新增了标签分类,或许可以帮助你更快地查看想看的内容。此外,也推荐使用我页面上的搜索功能利用关键词查找,非常便捷。

注意:PC体验更佳~

为了提高访问速度,我对我的博客进行了github+coding双线部署,url如下:
github page:https://gsy00517.github.io/
coding page:http://gsy00517.coding.me/
大家可以择优访问,事实上我并没有感到github的速度比coding慢。此外,由于我是同一个源文件双线部署,因此选择了把所有功能优先应用在github page上(比如文章打分、文章链接),但其实coding page也没有太大的区别。

注意:前期由于coding升级导致个人版page下线,好在可以迁移至新的coding,然而我尚未处理。不过github page可正常访问,也推荐访问我的github page。


关于订阅

首先,我想说的是,如果你觉得我写的内容或者方向对你有一点用处的话,非常欢迎收藏或订阅我的博客!如果你也在写博客的话,我们可以互相关注!
在侧栏,你可能会发现这样一个图标。点击之后,你会进入一个看不懂的atom.xml文件。

其实,看不懂是正常的,因为这个是给电脑看的。一个便捷的办法就是使用chrome的扩展程序添加feed(或者使用一些具有同样功能的手机app),然后打开网页时,就可以直接点击浏览器右上角的图标(会显示加号)进行订阅。这样以后每次更新了新的博文,你就可以收到提醒。


此外还可以像收藏其他网站一样进行收藏,这里不详述了。
欢迎大家常来踩踩,也欢迎大家留下评论。评论很简单,无需登录任何账号直接评论即可~
我目前也还在学习的过程中,欢迎大家和我交流,也欢迎各种批评与建议,我会努力改进!


Good News

好消息!好消息!本站已和百度、谷歌等世界知名搜索引擎达成战略合作关系!如果我写的文章有不足或疏漏之处、看完后有费解有困惑,都可以直接问度娘和谷哥就好啦~
哈哈哈皮这一下很开心。


碰到底线咯 后面没有啦
0%
python笔记:打印进度 | 高深远的博客

python笔记:打印进度

在我们训练模型的时候,我们总希望能够直接看到训练的进度,下面我就总结几个我收集的打印进度的方法。

References

电子文献:
https://blog.csdn.net/u013985241/article/details/86653356
https://blog.csdn.net/zkp_987/article/details/81748098


利用回车符

打印百分比应该是最常见的方法,也是我一直使用的。不过如果简单地逐次打印百分比的话,就会占据大量的屏幕空间,甚至装不下而需要手动拖动滚动条,让人眼花缭乱。这时我就想到了利用转义符“\r”,在print完本次的进度之后,下一次直接回车将其清除覆盖,这样就达到了既不占用屏幕又清晰的目的。
大致的方法如下:

1
2
3
4
5
import time #这里是为了用来延时,代替训练的时间
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
for i in range(numOfTimes):
print("\r", "progress percentage:{0}%".format((round(i + 1) * 100 / numOfTimes)), end = "", flush = True)
time.sleep(0.02) #若前面from time import sleep,这里直接sleep(0.02)即可

这里用到了python的format格式化函数,format中计算出的数值对应的位置是{0},将在实际print的过程中被替换。
此外,这里还用到了round()函数,其作用是返回浮点数的四舍五入值。
关于上面在print()函数中出现的flush,文首的参考链接中已给出解释,这里做个搬运:
因为print()函数会把内容放到内存中,内存中的内容并不一定能够及时刷新显示到屏幕中。而当我们使用flush = True之后,会在print结束之后,立即将内存中的东西显示到屏幕上,清空缓存。
基于上述原理,flush大致有下面两个使用场景:

  1. 在循环中,要想每进行一次循环体,在屏幕上更新打印的内容就得使用flush = True的参数设置。(我这里就是这种情况)
  2. 打开一个文件,向其写入字符串,在关闭文件f.close()之前 打开文件是看不到写入的字符的。因此,如果要想在关闭之前实时地看到写入的字符串,那么就应该使用flush = True

利用tqdm库

有需求就有市场,一搜果然还是有库能满足我的需求的。tqdm就是其中之一,它是一个快速,可扩展的python进度条,可以在python长循环中添加一个进度提示信息。
大致用法如下:

1
2
3
4
5
6
import tqdm
import time
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
for i in tqdm.tqdm(range(numOfTimes)):
time.sleep(0.02) #代替训练等耗时过程
pass

也可以直接from tqdm import tqdm,这样后面就不需要tqdm.tqdm了。


利用progressbar

库如其名,这个库就是用来做进度条的。如果没有的话,它和tqdm都可以使用pip来安装。

1
2
3
4
5
6
import progressbar
from time import sleep
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
progress = progressbar.ProgressBar()
for i in progress(range(numOfTimes)):
sleep(0.02)


碰到底线咯 后面没有啦

本文标题:python笔记:打印进度

文章作者:高深远

发布时间:2020年01月08日 - 21:40

最后更新:2020年02月07日 - 17:36

原始链接:https://gsy00517.github.io/python20200108214052/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
python笔记:打印进度 | 高深远的博客

python笔记:打印进度

在我们训练模型的时候,我们总希望能够直接看到训练的进度,下面我就总结几个我收集的打印进度的方法。

References

电子文献:
https://blog.csdn.net/u013985241/article/details/86653356
https://blog.csdn.net/zkp_987/article/details/81748098


利用回车符

打印百分比应该是最常见的方法,也是我一直使用的。不过如果简单地逐次打印百分比的话,就会占据大量的屏幕空间,甚至装不下而需要手动拖动滚动条,让人眼花缭乱。这时我就想到了利用转义符“\r”,在print完本次的进度之后,下一次直接回车将其清除覆盖,这样就达到了既不占用屏幕又清晰的目的。
大致的方法如下:

1
2
3
4
5
import time #这里是为了用来延时,代替训练的时间
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
for i in range(numOfTimes):
print("\r", "progress percentage:{0}%".format((round(i + 1) * 100 / numOfTimes)), end = "", flush = True)
time.sleep(0.02) #若前面from time import sleep,这里直接sleep(0.02)即可

这里用到了python的format格式化函数,format中计算出的数值对应的位置是{0},将在实际print的过程中被替换。
此外,这里还用到了round()函数,其作用是返回浮点数的四舍五入值。
关于上面在print()函数中出现的flush,文首的参考链接中已给出解释,这里做个搬运:
因为print()函数会把内容放到内存中,内存中的内容并不一定能够及时刷新显示到屏幕中。而当我们使用flush = True之后,会在print结束之后,立即将内存中的东西显示到屏幕上,清空缓存。
基于上述原理,flush大致有下面两个使用场景:

  1. 在循环中,要想每进行一次循环体,在屏幕上更新打印的内容就得使用flush = True的参数设置。(我这里就是这种情况)
  2. 打开一个文件,向其写入字符串,在关闭文件f.close()之前 打开文件是看不到写入的字符的。因此,如果要想在关闭之前实时地看到写入的字符串,那么就应该使用flush = True

利用tqdm库

有需求就有市场,一搜果然还是有库能满足我的需求的。tqdm就是其中之一,它是一个快速,可扩展的python进度条,可以在python长循环中添加一个进度提示信息。
大致用法如下:

1
2
3
4
5
6
import tqdm
import time
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
for i in tqdm.tqdm(range(numOfTimes)):
time.sleep(0.02) #代替训练等耗时过程
pass

也可以直接from tqdm import tqdm,这样后面就不需要tqdm.tqdm了。


利用progressbar

库如其名,这个库就是用来做进度条的。如果没有的话,它和tqdm都可以使用pip来安装。

1
2
3
4
5
6
import progressbar
from time import sleep
numOfTimes = 200 #总循环次数,可以是总训练数据量等,这里设为200
progress = progressbar.ProgressBar()
for i in progress(range(numOfTimes)):
sleep(0.02)


碰到底线咯 后面没有啦

本文标题:python笔记:打印进度

文章作者:高深远

发布时间:2020年01月08日 - 21:40

最后更新:2020年02月07日 - 17:36

原始链接:https://gsy00517.github.io/python20200108214052/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
python笔记:pandas基本使用 | 高深远的博客

python笔记:pandas基本使用

在python中,Pandas可以说是最实用的库之一,它提供了非常丰富的数据读写方法。可以看一下Pandas中文网提供的Pandas参考文档中对所有I/O函数的总结。

Pandas是一个开源的,BSD许可的库,为python提供高性能、易于使用的数据结构和数据分析工具。它的使用基础是Numpy(提供高性能的矩阵运算);可以用于数据挖掘和数据分析,同时也提供数据清洗的功能。
本文就对Pandas的基本使用做一个简单的归纳,所有代码可以从上往下按顺序依次执行。

References

电子文献:
https://www.cnblogs.com/chenhuabin/p/11477076.html
https://blog.csdn.net/weixin_39791387/article/details/81487549
https://kanoki.org/2019/09/16/dataframe-visualization-with-pandas-plot/
https://pandas.pydata.org/pandas-docs/stable/getting_started/10min.html
https://blog.csdn.net/weixin_41712499/article/details/82719987


csv

这里我想介绍一下一种新的数据格式:csv。它和excel很像,但又不同于excel。csv主要有如下特点:

  1. 纯文本,使用某个字符集,比如ASCII、Unicode、EBCDIC或GB2312(简体中文环境)等;
  2. 由记录组成(典型的是每行一条记录);
  3. 每条记录被分隔符(英语:Delimiter)分隔为字段(英语:Field (computer science))(典型分隔符有逗号、分号或制表符;有时分隔符可以包括可选的空格);
  4. 每条记录都有同样的字段序列。

在Pandas的使用以及AI相关竞赛数据集、结果的存储与使用中,csv文件往往承担着主角的位置。


准备

在具体使用之前,别忘了先导入所需相应的库。

1
2
import pandas as pd
import numpy as np

可以使用pd.__version__来输出版本号,注意,这里的“__”是两个“_”,这个很容易搞错且难以发现。


两大利器

Pandas中文网首页,在介绍完Pandas之后,就重点介绍了一下Pandas的两大利器。分别是DataFrame和Series。这里我先介绍一下Seires,DataFrame在后面有更详细的操作。

  1. Series简介

    Series是一种类似于一维数组的对象,是由一组数据(各种NumPy数据类型)以及一组与之相关的数据标签(即索引)组成。仅由一组数据也可产生简单的Series对象。
    我们可以通过传入一个list的数值来创建一个Series,Pandas会创建一个默认的整数索引:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    s = pd.Series([1, 3, 5, np.nan, 6, 8])

    s
    >>> 0 1.0
    1 3.0
    2 5.0
    3 NaN
    4 6.0
    5 8.0
    dtype: float64

    注:这里用np.nan来产生NaN,但要注意的是np.nan不是一个“空”对象,即使用np.nan == np.nan来判断将返回False,np.nan的类型为基本数据类型float。
    若要对某个值进行空值判断,如对np.nan,需要用np.isnan(np.nan),此时返回为True。

    另外,也可以从字典创建Series。

  2. DataFrame简介

    DataFrame是Pandas中的一个表格型的数据结构,包含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型等),DataFrame即有行索引也有列索引,可以被看做是由Series组成的字典。
    我们可以通过传入一个numpy数组来创建一个DataFrame,如下面带有一个datetime的索引以及被标注的列:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    dates = pd.date_range('20130101', periods = 6)

    dates
    >>> DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04',
    '2013-01-05', '2013-01-06'],
    dtype = 'datetime64[ns]', freq = 'D')

    df1 = pd.DataFrame(np.random.randn(6, 4), index = dates, columns = list('ABCD'))

    df1
    >>> A B C D
    2013-01-01 0.469112 -0.282863 -1.509059 -1.135632
    2013-01-02 1.212112 -0.173215 0.119209 -1.044236
    2013-01-03 -0.861849 -2.104569 -0.494929 1.071804
    2013-01-04 0.721555 -0.706771 -1.039575 0.271860
    2013-01-05 -0.424972 0.567020 0.276232 -1.087401
    2013-01-06 -0.673690 0.113648 -1.478427 0.524988

    注:上面用pd.data_range()生成了一个时间频率freq = 'D'(即天)的日期序列。

    我们也可以通过传入一个可以转换为类Series(series-like)的字典对象来创建一个DataFrame:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    df2 = pd.DataFrame({'A': 1.,
    'B': pd.Timestamp('20130102'),
    'C': pd.Series(1, index = list(range(4)), dtype = 'float32'),
    'D': np.array([3] * 4, dtype = 'int32'),
    'E': pd.Categorical(["test", "train", "test", "train"]),
    'F': 'foo'})

    df2
    >>> A B C D E F
    0 1.0 2013-01-02 1.0 3 test foo
    1 1.0 2013-01-02 1.0 3 train foo
    2 1.0 2013-01-02 1.0 3 test foo
    3 1.0 2013-01-02 1.0 3 train foo

    这里可以使用df2.dtypes来查看不同列的数据类型。

读取

无论是txt文件还是csv文件,在Pandas中都使用read_csv()读取,当然也使用同一个方法写入到文件,那就是to_csv()方法。

1
df = pd.read_csv(abs_path) #此为绝对路径

为了提供更加多样化、可定制的功能,read_csv()方法定义了数十个参数,还在大部分参数并不常用,以下是几个比较常用的参数:

  1. filepath_or_buffer:文件所在路径,可以是一个描述路径的字符串、pathlib.Path对象、http或ftp的连接,也可以是任何可调用read()的对象。这是唯一一个必传的参数,也就是上面的abs_path。
  2. encoding:编码,字符型,通常为utf-8,如果中文读取不正常,可以将encoding设为gbk。当然,也可以直接将对应文件改成utf-8编码。
  3. header:整数或者由整数组成的列表,用来指定由哪一行或者哪几行作为列名,默认为header = 0,表示用第一列作为列名。若设置header = 0,则指定第二列作为列名。要注意的是,当指定第一行之后的数据作为列名时,前面的所有行都会被略过。也可以传递一个包含多个整数的列表给header,这样每一列就会有多个列名。如果中间某一行没有指定,那么该行会被略过。例如header = [0, 2],则原本的第二行会被省去。而当文件中没有列名一行数据时,可以传递header = None,表示不从文件数据中指定行作为列名,这时Pandas会自动生成从零开始的序列作为列名。
  4. names:接着上面的header,很快就想到是不是可以自己设置列名。names就可以用来生成一个列表,为数据额外指定列名。例如:df = pd.read_csv('abs_path, names=['第一列', '第二列', '第三列', '第四列'])

在数据读取完毕之后,我们可以使用如下代码来快速查看数据是否正确地导入了。

1
2
3
4
df.head() #看一下导入后df(DataFrame)的前几行,可在括号内输入数字来设定具体显示几行,默认5行
df.tail() #类似,查看后几行

type(df) #查看类型,DataFrame的输出应该是pandas.core.frame.DataFrame


DataFrame

DataFrame的介绍在前面的简介已经写过,这里就不赘述了。事实上,Pandas中的DataFrame的操作,有很大一部分跟numpy中的二维数组的操作是近似的。
在上面的读取处理之后,我们下面对其进行一些简单的操作:

  1. 查看

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    df.head() #上文已提及
    df.tail()

    #查看列名
    print(df.columns)

    #查看索引
    print(df.index)

    #查看各列的数据格式
    print(df.dtypes)

    #查看整个DataFrame的属性信息
    print(df.info())

    #访问对应行
    df.loc[0] #这里访问了第一行,将显示列名和对应每一列第一行的数据
    #具体有关索引请看后文
  2. 筛选

    在numpy中,我们可以这样判断一个数组中每一个数和对应数值的比较结果:

    1
    2
    a = np.array(range(10))
    a > 3

    输出将是一串布尔型(True、False)的array。
    而在DataFrame中,我们可以用类似的方法通过指定列来进行筛选:

    1
    2
    #筛选第二列中数值大于80
    df[df.第二列 > 80]

    这样就会得到只用符合条件数据的对应行的一个DataFrame。
    我们也可以使用df[(df.第一列 > 80) & (df.第二列 > 80) & (df.第三列 > 80)]来进行多条件的复杂筛选。
    此外,我们可以直接根据列名提取出一个新的表格:

    1
    new = df[['第一列', '第二列']] #new为仅由第一列和第二列组成的一个新的DataFrame
  3. 排序

    可以使用如下代码根据单列或者多列的值对数据进行排序:

    1
    2
    df.sort_values(['第二列', '第一列', '第三列'], ascending = [True, True, True])
    #使用df.sort_values(['第二列', '第一列', '第三列']).head()查看排序完后前几行的结果

    这里排序的规则是:根据设置的顺序(这里是先按第二列排),从小到大升序对所有数据进行排序。其中ascending是设置升序(默认True)和降序(False)。若仅选择单列,则无需添加[],这里[]的作用是把选择的行列转换为列表。

  4. 重命名

    如果觉得我前面取得列名称不好听,可以使用下面这个代码来改成需要的名字:

    1
    df.rename(columns = {'第一列': '好听的第一列', '第二列': '好听的第二列', '第三列': '好听的第三列', '第四列': '好听的第四列',}, inplace = True)

    这里用到了字典。

  5. 索引

    前面提到了使用索引来查看第一行,可当没有数字索引,例如我们通过df = pd.DataFrame(scores, index = ['one', 'two', 'three'])把index设为one、two、three时,df.loc[0]就失效了。因此有下面几种处理方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #访问index为“one”的行
    df.loc['one']

    #访问实实在在所谓的第几行(无论index为何)
    df.iloc[0] #注意0指的是第一行

    #ix合并了loc和iloc的功能,当索引为数字索引的时候,ix和loc是等价的
    df.ix[0] #访问第一行
    df.ix['one'] #访问“one”行,这里也指的是第一行
  6. 切片

    类似的,DataFrame也支持切片操作,但还是需要注意的。
    这里总结两种切片方式:

    1. 利用索引

      即使用df.loc[:2]ordf.ix[:2]等索引方式,这里这样的话输出为前三行。
    2. 直接切片

      这种方法只能在访问多行数据时使用,例如df[:2]将输出前两行,注意,这里比上面的方法要少一行。此外,值得强调的是,用这种方法访问单行数据是禁止的,例如不能使用df[0]来访问第一行数据。
  7. 插入

    上面的索引还有一种用途,就是可以用于插入指定index的新行。

    1
    df.loc['new_index'] = ['one', 'piece', 'is', 'true']
  8. 删除

    上面插入的那行中我说了“大秘宝是真实存在的”(海贼迷懂),下面我想把这句话所在的行删了,可以使用df.dtop()来完成。

    1
    df = df.drop('new_index')
  9. 数组

    我们可以使用df.第一列.values以array的形式输出指定列的所用值。
    基于此,我们可以使用df.第一列.value_counts()来做简单的统计,也就是对该列中每一个出现数字作频次的统计。
    我们还可以直接对DataFrame做计算,例如:df * n(n为具体数值),结果就是对表中的每一个数值都乘上对应的倍数。

  10. 元素操作

    1. map函数

      map()是python自带的方法, 可以对DataFrame某列内的元素进行操作。
      下面是一种使用实例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      def func(grade):
      if grade >= 80:
      return "A"
      elif grade >= 70:
      return "B"
      elif grade >= 60:
      return "C"
      else:
      return "D"

      df['评级'] = df.第一列.map(func)

      这样DataFrame后面会自动添加一列名为“评级”,并根据第一列来生成数据填入。

    2. apply函数

      当我们需要进行根据多列生成新的一个列的操作时,就需要用到apply。其用法简单示例如下:

      1
      df['求和'] = df.apply(lambda x: x.第一列 + x.第二列, axis = 1)
    3. applymap函数

      applymap时对dataframe中所有的数据进行操作的一个函数,非常重要。例如,我要让之前所用的score和grade都变成scroe+或者grade+,那么我就可以这样:

      1
      df.applymap(lambda x: str(x) + '+')

      如果是成绩单的话,那么这样操作之后打出来就会好看些啦,哈哈。

  11. plot

    数据可视化本来是一个非常复杂的过程,但Pandas数据帧plot函数的出现,使得创建可视化图形变得很容易。
    这个函数的具体使用可以访问文首给出的第三个参考链接,为一个印度小哥利用kaggle上的数据df.plot()做的一个非常详尽的介绍。
    有机会的话我会结合matplotlib对其做一个搬运与总结,先留个坑。
  12. 统计

    我们可以使用df.describe()对数据进行快速统计汇总。输出将包括:count、mean、std、min、25%、50%、75%和max。
    通过df.mean()我们可以按列求均值,如果想要按行求均值,可以使用df.mean(1)
  13. 高阶

    此外还有使用df.T进行转置,df.dropna(how = 'any')删除所有具有缺失值的数据,df.fillna(value = 5)填充所有缺失数据等高阶用法。详细的可以查阅前面给的参考链接10 minutes to pandas,至于更高阶的,可以看一下cookbook,不过一般还是在运用的过程中遇到需求再查找,一下子记不住那么多的。

写入

通过to_csv()可以将Pandas数据写入到文本文件中,和读取read_csv()类似,它也有几个常用参数:

  1. path_or_buf:表示路径的字符串或者文件句柄,也是必需的。例如:df.to_csv(abs_path)。要注意的是,这里如果abs_path对应的文件不存在,则会新建abs_path的同名文件后再写入,如果本来已存在该文件,则会自动清空该文件后再写入。
  2. sep:分隔符,默认为逗号。当写入txt文件时,就需要这个参数来确定数据之间的分隔符了。
  3. header:元素为字符串的列表或布尔型数据。当为列表时表示重新指定列名,当为布尔型时,表示是否写入列名。这和读取时的使用基本类似。
  4. columns:后接一个列表,用于重新指定写入文件中列的顺序。例如:df.to_csv(abs_path, columns = ['第四列', '第二列', '第三列', '第一列'])
  5. index_label:字符串或布尔型变量,设置索引列列名。原本的索引是空的,使用这个参数就可以给索引添加一个列名。如果觉得不需要添加,同时空着不好看(空的话还是会有分隔符),可以设置为False去掉(同时也将不显示分隔符)。
  6. index:布尔型,是否写入索引列,默认为True。
  7. encoding:写入时所用的编码,默认是utf-8。这个和上述的许多参数其实保持默认即可。

匿名函数

在上面DataFrame一节的最后,我用到了两个匿名函数,这里我想举个例子来简单展示一下匿名函数的使用方法,的确很好用!
当我们对一个数进行操作时,若使用函数,一般会:

1
2
def func(number):
return number + 10

这样看上去就有点费代码了,因此有下面的等价匿名函数可以替代:

1
func = lambda number: number + 10

当然假如想追求代码行数的话也不拦着你~


推荐

最近看到了DataWhale的一篇文章,也总结的挺好,在这里推荐一下。


碰到底线咯 后面没有啦

本文标题:python笔记:pandas基本使用

文章作者:高深远

发布时间:2020年01月13日 - 20:28

最后更新:2020年02月01日 - 17:35

原始链接:https://gsy00517.github.io/python20200113202851/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
python笔记:pandas基本使用 | 高深远的博客

python笔记:pandas基本使用

在python中,Pandas可以说是最实用的库之一,它提供了非常丰富的数据读写方法。可以看一下Pandas中文网提供的Pandas参考文档中对所有I/O函数的总结。

Pandas是一个开源的,BSD许可的库,为python提供高性能、易于使用的数据结构和数据分析工具。它的使用基础是Numpy(提供高性能的矩阵运算);可以用于数据挖掘和数据分析,同时也提供数据清洗的功能。
本文就对Pandas的基本使用做一个简单的归纳,所有代码可以从上往下按顺序依次执行。

References

电子文献:
https://www.cnblogs.com/chenhuabin/p/11477076.html
https://blog.csdn.net/weixin_39791387/article/details/81487549
https://kanoki.org/2019/09/16/dataframe-visualization-with-pandas-plot/
https://pandas.pydata.org/pandas-docs/stable/getting_started/10min.html
https://blog.csdn.net/weixin_41712499/article/details/82719987


csv

这里我想介绍一下一种新的数据格式:csv。它和excel很像,但又不同于excel。csv主要有如下特点:

  1. 纯文本,使用某个字符集,比如ASCII、Unicode、EBCDIC或GB2312(简体中文环境)等;
  2. 由记录组成(典型的是每行一条记录);
  3. 每条记录被分隔符(英语:Delimiter)分隔为字段(英语:Field (computer science))(典型分隔符有逗号、分号或制表符;有时分隔符可以包括可选的空格);
  4. 每条记录都有同样的字段序列。

在Pandas的使用以及AI相关竞赛数据集、结果的存储与使用中,csv文件往往承担着主角的位置。


准备

在具体使用之前,别忘了先导入所需相应的库。

1
2
import pandas as pd
import numpy as np

可以使用pd.__version__来输出版本号,注意,这里的“__”是两个“_”,这个很容易搞错且难以发现。


两大利器

Pandas中文网首页,在介绍完Pandas之后,就重点介绍了一下Pandas的两大利器。分别是DataFrame和Series。这里我先介绍一下Seires,DataFrame在后面有更详细的操作。

  1. Series简介

    Series是一种类似于一维数组的对象,是由一组数据(各种NumPy数据类型)以及一组与之相关的数据标签(即索引)组成。仅由一组数据也可产生简单的Series对象。
    我们可以通过传入一个list的数值来创建一个Series,Pandas会创建一个默认的整数索引:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    s = pd.Series([1, 3, 5, np.nan, 6, 8])

    s
    >>> 0 1.0
    1 3.0
    2 5.0
    3 NaN
    4 6.0
    5 8.0
    dtype: float64

    注:这里用np.nan来产生NaN,但要注意的是np.nan不是一个“空”对象,即使用np.nan == np.nan来判断将返回False,np.nan的类型为基本数据类型float。
    若要对某个值进行空值判断,如对np.nan,需要用np.isnan(np.nan),此时返回为True。

    另外,也可以从字典创建Series。

  2. DataFrame简介

    DataFrame是Pandas中的一个表格型的数据结构,包含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型等),DataFrame即有行索引也有列索引,可以被看做是由Series组成的字典。
    我们可以通过传入一个numpy数组来创建一个DataFrame,如下面带有一个datetime的索引以及被标注的列:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    dates = pd.date_range('20130101', periods = 6)

    dates
    >>> DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04',
    '2013-01-05', '2013-01-06'],
    dtype = 'datetime64[ns]', freq = 'D')

    df1 = pd.DataFrame(np.random.randn(6, 4), index = dates, columns = list('ABCD'))

    df1
    >>> A B C D
    2013-01-01 0.469112 -0.282863 -1.509059 -1.135632
    2013-01-02 1.212112 -0.173215 0.119209 -1.044236
    2013-01-03 -0.861849 -2.104569 -0.494929 1.071804
    2013-01-04 0.721555 -0.706771 -1.039575 0.271860
    2013-01-05 -0.424972 0.567020 0.276232 -1.087401
    2013-01-06 -0.673690 0.113648 -1.478427 0.524988

    注:上面用pd.data_range()生成了一个时间频率freq = 'D'(即天)的日期序列。

    我们也可以通过传入一个可以转换为类Series(series-like)的字典对象来创建一个DataFrame:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    df2 = pd.DataFrame({'A': 1.,
    'B': pd.Timestamp('20130102'),
    'C': pd.Series(1, index = list(range(4)), dtype = 'float32'),
    'D': np.array([3] * 4, dtype = 'int32'),
    'E': pd.Categorical(["test", "train", "test", "train"]),
    'F': 'foo'})

    df2
    >>> A B C D E F
    0 1.0 2013-01-02 1.0 3 test foo
    1 1.0 2013-01-02 1.0 3 train foo
    2 1.0 2013-01-02 1.0 3 test foo
    3 1.0 2013-01-02 1.0 3 train foo

    这里可以使用df2.dtypes来查看不同列的数据类型。

读取

无论是txt文件还是csv文件,在Pandas中都使用read_csv()读取,当然也使用同一个方法写入到文件,那就是to_csv()方法。

1
df = pd.read_csv(abs_path) #此为绝对路径

为了提供更加多样化、可定制的功能,read_csv()方法定义了数十个参数,还在大部分参数并不常用,以下是几个比较常用的参数:

  1. filepath_or_buffer:文件所在路径,可以是一个描述路径的字符串、pathlib.Path对象、http或ftp的连接,也可以是任何可调用read()的对象。这是唯一一个必传的参数,也就是上面的abs_path。
  2. encoding:编码,字符型,通常为utf-8,如果中文读取不正常,可以将encoding设为gbk。当然,也可以直接将对应文件改成utf-8编码。
  3. header:整数或者由整数组成的列表,用来指定由哪一行或者哪几行作为列名,默认为header = 0,表示用第一列作为列名。若设置header = 0,则指定第二列作为列名。要注意的是,当指定第一行之后的数据作为列名时,前面的所有行都会被略过。也可以传递一个包含多个整数的列表给header,这样每一列就会有多个列名。如果中间某一行没有指定,那么该行会被略过。例如header = [0, 2],则原本的第二行会被省去。而当文件中没有列名一行数据时,可以传递header = None,表示不从文件数据中指定行作为列名,这时Pandas会自动生成从零开始的序列作为列名。
  4. names:接着上面的header,很快就想到是不是可以自己设置列名。names就可以用来生成一个列表,为数据额外指定列名。例如:df = pd.read_csv('abs_path, names=['第一列', '第二列', '第三列', '第四列'])

在数据读取完毕之后,我们可以使用如下代码来快速查看数据是否正确地导入了。

1
2
3
4
df.head() #看一下导入后df(DataFrame)的前几行,可在括号内输入数字来设定具体显示几行,默认5行
df.tail() #类似,查看后几行

type(df) #查看类型,DataFrame的输出应该是pandas.core.frame.DataFrame


DataFrame

DataFrame的介绍在前面的简介已经写过,这里就不赘述了。事实上,Pandas中的DataFrame的操作,有很大一部分跟numpy中的二维数组的操作是近似的。
在上面的读取处理之后,我们下面对其进行一些简单的操作:

  1. 查看

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    df.head() #上文已提及
    df.tail()

    #查看列名
    print(df.columns)

    #查看索引
    print(df.index)

    #查看各列的数据格式
    print(df.dtypes)

    #查看整个DataFrame的属性信息
    print(df.info())

    #访问对应行
    df.loc[0] #这里访问了第一行,将显示列名和对应每一列第一行的数据
    #具体有关索引请看后文
  2. 筛选

    在numpy中,我们可以这样判断一个数组中每一个数和对应数值的比较结果:

    1
    2
    a = np.array(range(10))
    a > 3

    输出将是一串布尔型(True、False)的array。
    而在DataFrame中,我们可以用类似的方法通过指定列来进行筛选:

    1
    2
    #筛选第二列中数值大于80
    df[df.第二列 > 80]

    这样就会得到只用符合条件数据的对应行的一个DataFrame。
    我们也可以使用df[(df.第一列 > 80) & (df.第二列 > 80) & (df.第三列 > 80)]来进行多条件的复杂筛选。
    此外,我们可以直接根据列名提取出一个新的表格:

    1
    new = df[['第一列', '第二列']] #new为仅由第一列和第二列组成的一个新的DataFrame
  3. 排序

    可以使用如下代码根据单列或者多列的值对数据进行排序:

    1
    2
    df.sort_values(['第二列', '第一列', '第三列'], ascending = [True, True, True])
    #使用df.sort_values(['第二列', '第一列', '第三列']).head()查看排序完后前几行的结果

    这里排序的规则是:根据设置的顺序(这里是先按第二列排),从小到大升序对所有数据进行排序。其中ascending是设置升序(默认True)和降序(False)。若仅选择单列,则无需添加[],这里[]的作用是把选择的行列转换为列表。

  4. 重命名

    如果觉得我前面取得列名称不好听,可以使用下面这个代码来改成需要的名字:

    1
    df.rename(columns = {'第一列': '好听的第一列', '第二列': '好听的第二列', '第三列': '好听的第三列', '第四列': '好听的第四列',}, inplace = True)

    这里用到了字典。

  5. 索引

    前面提到了使用索引来查看第一行,可当没有数字索引,例如我们通过df = pd.DataFrame(scores, index = ['one', 'two', 'three'])把index设为one、two、three时,df.loc[0]就失效了。因此有下面几种处理方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #访问index为“one”的行
    df.loc['one']

    #访问实实在在所谓的第几行(无论index为何)
    df.iloc[0] #注意0指的是第一行

    #ix合并了loc和iloc的功能,当索引为数字索引的时候,ix和loc是等价的
    df.ix[0] #访问第一行
    df.ix['one'] #访问“one”行,这里也指的是第一行
  6. 切片

    类似的,DataFrame也支持切片操作,但还是需要注意的。
    这里总结两种切片方式:

    1. 利用索引

      即使用df.loc[:2]ordf.ix[:2]等索引方式,这里这样的话输出为前三行。
    2. 直接切片

      这种方法只能在访问多行数据时使用,例如df[:2]将输出前两行,注意,这里比上面的方法要少一行。此外,值得强调的是,用这种方法访问单行数据是禁止的,例如不能使用df[0]来访问第一行数据。
  7. 插入

    上面的索引还有一种用途,就是可以用于插入指定index的新行。

    1
    df.loc['new_index'] = ['one', 'piece', 'is', 'true']
  8. 删除

    上面插入的那行中我说了“大秘宝是真实存在的”(海贼迷懂),下面我想把这句话所在的行删了,可以使用df.dtop()来完成。

    1
    df = df.drop('new_index')
  9. 数组

    我们可以使用df.第一列.values以array的形式输出指定列的所用值。
    基于此,我们可以使用df.第一列.value_counts()来做简单的统计,也就是对该列中每一个出现数字作频次的统计。
    我们还可以直接对DataFrame做计算,例如:df * n(n为具体数值),结果就是对表中的每一个数值都乘上对应的倍数。

  10. 元素操作

    1. map函数

      map()是python自带的方法, 可以对DataFrame某列内的元素进行操作。
      下面是一种使用实例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      def func(grade):
      if grade >= 80:
      return "A"
      elif grade >= 70:
      return "B"
      elif grade >= 60:
      return "C"
      else:
      return "D"

      df['评级'] = df.第一列.map(func)

      这样DataFrame后面会自动添加一列名为“评级”,并根据第一列来生成数据填入。

    2. apply函数

      当我们需要进行根据多列生成新的一个列的操作时,就需要用到apply。其用法简单示例如下:

      1
      df['求和'] = df.apply(lambda x: x.第一列 + x.第二列, axis = 1)
    3. applymap函数

      applymap时对dataframe中所有的数据进行操作的一个函数,非常重要。例如,我要让之前所用的score和grade都变成scroe+或者grade+,那么我就可以这样:

      1
      df.applymap(lambda x: str(x) + '+')

      如果是成绩单的话,那么这样操作之后打出来就会好看些啦,哈哈。

  11. plot

    数据可视化本来是一个非常复杂的过程,但Pandas数据帧plot函数的出现,使得创建可视化图形变得很容易。
    这个函数的具体使用可以访问文首给出的第三个参考链接,为一个印度小哥利用kaggle上的数据df.plot()做的一个非常详尽的介绍。
    有机会的话我会结合matplotlib对其做一个搬运与总结,先留个坑。
  12. 统计

    我们可以使用df.describe()对数据进行快速统计汇总。输出将包括:count、mean、std、min、25%、50%、75%和max。
    通过df.mean()我们可以按列求均值,如果想要按行求均值,可以使用df.mean(1)
  13. 高阶

    此外还有使用df.T进行转置,df.dropna(how = 'any')删除所有具有缺失值的数据,df.fillna(value = 5)填充所有缺失数据等高阶用法。详细的可以查阅前面给的参考链接10 minutes to pandas,至于更高阶的,可以看一下cookbook,不过一般还是在运用的过程中遇到需求再查找,一下子记不住那么多的。

写入

通过to_csv()可以将Pandas数据写入到文本文件中,和读取read_csv()类似,它也有几个常用参数:

  1. path_or_buf:表示路径的字符串或者文件句柄,也是必需的。例如:df.to_csv(abs_path)。要注意的是,这里如果abs_path对应的文件不存在,则会新建abs_path的同名文件后再写入,如果本来已存在该文件,则会自动清空该文件后再写入。
  2. sep:分隔符,默认为逗号。当写入txt文件时,就需要这个参数来确定数据之间的分隔符了。
  3. header:元素为字符串的列表或布尔型数据。当为列表时表示重新指定列名,当为布尔型时,表示是否写入列名。这和读取时的使用基本类似。
  4. columns:后接一个列表,用于重新指定写入文件中列的顺序。例如:df.to_csv(abs_path, columns = ['第四列', '第二列', '第三列', '第一列'])
  5. index_label:字符串或布尔型变量,设置索引列列名。原本的索引是空的,使用这个参数就可以给索引添加一个列名。如果觉得不需要添加,同时空着不好看(空的话还是会有分隔符),可以设置为False去掉(同时也将不显示分隔符)。
  6. index:布尔型,是否写入索引列,默认为True。
  7. encoding:写入时所用的编码,默认是utf-8。这个和上述的许多参数其实保持默认即可。

匿名函数

在上面DataFrame一节的最后,我用到了两个匿名函数,这里我想举个例子来简单展示一下匿名函数的使用方法,的确很好用!
当我们对一个数进行操作时,若使用函数,一般会:

1
2
def func(number):
return number + 10

这样看上去就有点费代码了,因此有下面的等价匿名函数可以替代:

1
func = lambda number: number + 10

当然假如想追求代码行数的话也不拦着你~


推荐

最近看到了DataWhale的一篇文章,也总结的挺好,在这里推荐一下。


碰到底线咯 后面没有啦

本文标题:python笔记:pandas基本使用

文章作者:高深远

发布时间:2020年01月13日 - 20:28

最后更新:2020年02月01日 - 17:35

原始链接:https://gsy00517.github.io/python20200113202851/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
pytorch笔记:张量的广播机制 | 高深远的博客

pytorch笔记:张量的广播机制

在pytorch的张量计算中,“广播”指的是当满足一定条件时,较小的张量能够自动扩张成合适尺寸的大张量,使得能够进行计算。

References

电子文献:
https://pytorch.org/docs/stable/notes/broadcasting.html

条件

当一对张量满足下面的条件时,它们才是可以被“广播”的。

  1. 每个张量至少有一个维度。
  2. 迭代维度尺寸时,从尾部(也就是从后往前)开始,依次每个维度的尺寸必须满足以下之一:
    • 相等
    • 其中一个张量的维度尺寸为1
    • 其中一个张量不存在这个维度。

例子

光看条件可能会有点迷,下面是官方文档中的几个例子。

1
import torch

首先,显然相同形状的张量是可以广播的(或者说不需要广播)。

1
2
x = torch.empty(5, 7, 3)
y = torch.empty(5, 7, 3)

(x+y).size()输出torch.Size([5, 7, 3])
一般而言,我们可以从后往前分析一对张量是不是“broadcastable”。

1
2
x = torch.empty(5, 3, 4, 1)
y = torch.empty( 3, 1, 1)

我们依次分析:
对倒数第一个维度,两者尺寸相同,符合“相等”的条件。
对倒数第二个维度,y的尺寸为1,符合“其中一个张量尺寸为1”的条件。
对倒数第三个维度,两者尺寸相同,符合“相等”的条件。
对倒数第四个维度,y的该维度不存在,符合“其中一个张量不存在这个维度”的条件。
综上,这两个张量可以广播。
其实,x可以比y多出更多的维度,都满足该维度有“其中一个张量不存在这个维度”的条件。
举个反例:

1
2
x = torch.empty(5, 2, 4, 1)
y = torch.empty( 3, 1, 1)

这里若x+y就无法广播了,因为从后往前遇到倒数第三个维度时会被卡住,不满足任何一个条件。
此外,对torch.empty((0,)),它也无法和其他任何张量广播,可以输出发现其结果为tensor([]),是空的,这就不满足“每个张量至少有一个维度”这个条件。


tensor.view()

这里做个补充,一般我们在全连接层,总是会看到对某个张量作view(-1, ...)的变换,其实这里的“-1”并不是直接表示尺寸,而是起到让计算机帮助自动计算的功能。
举个例子,比如一个tensor含有6个数据(无论唯独尺寸如何组合),我们对其view(-1, 2),那么输出的尺寸就是[3, 2],若view(-1),那么输出的尺寸就是[6]。
注意,不能同时出现两个“-1”让机器计算。虽然在某些情况下,人能够判断,比如含有6个数据的tensor我们能够想到view(-1, -1, 6)输出尺寸是[1, 1, 6],但机器就会认为这是个错误。


碰到底线咯 后面没有啦

本文标题:pytorch笔记:张量的广播机制

文章作者:高深远

发布时间:2020年02月28日 - 11:14

最后更新:2020年02月29日 - 13:14

原始链接:https://gsy00517.github.io/pytorch20200228111430/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
pytorch笔记:张量的广播机制 | 高深远的博客

pytorch笔记:张量的广播机制

在pytorch的张量计算中,“广播”指的是当满足一定条件时,较小的张量能够自动扩张成合适尺寸的大张量,使得能够进行计算。

References

电子文献:
https://pytorch.org/docs/stable/notes/broadcasting.html

条件

当一对张量满足下面的条件时,它们才是可以被“广播”的。

  1. 每个张量至少有一个维度。
  2. 迭代维度尺寸时,从尾部(也就是从后往前)开始,依次每个维度的尺寸必须满足以下之一:
    • 相等
    • 其中一个张量的维度尺寸为1
    • 其中一个张量不存在这个维度。

例子

光看条件可能会有点迷,下面是官方文档中的几个例子。

1
import torch

首先,显然相同形状的张量是可以广播的(或者说不需要广播)。

1
2
x = torch.empty(5, 7, 3)
y = torch.empty(5, 7, 3)

(x+y).size()输出torch.Size([5, 7, 3])
一般而言,我们可以从后往前分析一对张量是不是“broadcastable”。

1
2
x = torch.empty(5, 3, 4, 1)
y = torch.empty( 3, 1, 1)

我们依次分析:
对倒数第一个维度,两者尺寸相同,符合“相等”的条件。
对倒数第二个维度,y的尺寸为1,符合“其中一个张量尺寸为1”的条件。
对倒数第三个维度,两者尺寸相同,符合“相等”的条件。
对倒数第四个维度,y的该维度不存在,符合“其中一个张量不存在这个维度”的条件。
综上,这两个张量可以广播。
其实,x可以比y多出更多的维度,都满足该维度有“其中一个张量不存在这个维度”的条件。
举个反例:

1
2
x = torch.empty(5, 2, 4, 1)
y = torch.empty( 3, 1, 1)

这里若x+y就无法广播了,因为从后往前遇到倒数第三个维度时会被卡住,不满足任何一个条件。
此外,对torch.empty((0,)),它也无法和其他任何张量广播,可以输出发现其结果为tensor([]),是空的,这就不满足“每个张量至少有一个维度”这个条件。


tensor.view()

这里做个补充,一般我们在全连接层,总是会看到对某个张量作view(-1, ...)的变换,其实这里的“-1”并不是直接表示尺寸,而是起到让计算机帮助自动计算的功能。
举个例子,比如一个tensor含有6个数据(无论唯独尺寸如何组合),我们对其view(-1, 2),那么输出的尺寸就是[3, 2],若view(-1),那么输出的尺寸就是[6]。
注意,不能同时出现两个“-1”让机器计算。虽然在某些情况下,人能够判断,比如含有6个数据的tensor我们能够想到view(-1, -1, 6)输出尺寸是[1, 1, 6],但机器就会认为这是个错误。


碰到底线咯 后面没有啦

本文标题:pytorch笔记:张量的广播机制

文章作者:高深远

发布时间:2020年02月28日 - 11:14

最后更新:2020年02月29日 - 13:14

原始链接:https://gsy00517.github.io/pytorch20200228111430/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
这些文章看得最多 | 高深远的博客

这些文章看得最多

注:由于LeanCloud访问原因,有时此页会出现无法正确显示的问题,请稍后重新刷新查看。

0%
这些文章看得最多 | 高深远的博客

这些文章看得最多

注:由于LeanCloud访问原因,有时此页会出现无法正确显示的问题,请稍后重新刷新查看。

0%
signal笔记:路由器天线摆放方法小实验 | 高深远的博客

signal笔记:路由器天线摆放方法小实验

这篇文章要从一只蝙蝠说起…好吧其实这是一篇没有任何技术含量的文章。纯粹心血来潮,研究了一下路由器的天线(我家是三根)的摆放方法与网速之间的关系。


起因

不知道怎么,总感觉家里“专属”的WiFi信号反而没流量快。上网一查,才发现我们在商场或者网上看到的路由器商品展示的天线摆放方法(全部竖直向上)是错误的。
根据网上资料,一般家用路由器的全向天线的无线信号分布类似被压扁的椭球形,无线信号在与天线垂直的方向上覆盖效果最好。由于手机的天线位于手机边框内、笔记本电脑的天线位于连接屏幕的水平转轴中…而设备的不同摆放方式也会导致接收信号的效果不同。后面我会证明天线朝同一个方向的叠加摆放与信号的增强没有必然联系,因此正确的摆法应该交叉而不指向同一方向。若是两根天线,则应成直角;若三根,则应如下图所示。


实验结果

为了取得比较可靠的数据,我借鉴了物理实验中交替重复测量的思想,分别进行了不同向、同向、再不同向、再同向…做了共8次实验,结果如下(仅展示其中4次)。




可以看到,两种摆法的延迟和下行速度并没有很大区别(全部竖直向上摆放下行稍快),而上行速度却有显著区别。可见天线朝同一个方向的叠加摆放与信号的增强没有必然联系(有说法说反而会干扰),而朝不同的方向确实利于接收信号,提高上行速度。不过一般我们使用网络大部分都是下行传递数据,总的来说,两种方法各有细微的利弊但在使用上区别不大。


碰到底线咯 后面没有啦

本文标题:signal笔记:路由器天线摆放方法小实验

文章作者:高深远

发布时间:2020年03月12日 - 17:22

最后更新:2020年03月12日 - 18:10

原始链接:https://gsy00517.github.io/signal20200312172247/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
signal笔记:路由器天线摆放方法小实验 | 高深远的博客

signal笔记:路由器天线摆放方法小实验

这篇文章要从一只蝙蝠说起…好吧其实这是一篇没有任何技术含量的文章。纯粹心血来潮,研究了一下路由器的天线(我家是三根)的摆放方法与网速之间的关系。


起因

不知道怎么,总感觉家里“专属”的WiFi信号反而没流量快。上网一查,才发现我们在商场或者网上看到的路由器商品展示的天线摆放方法(全部竖直向上)是错误的。
根据网上资料,一般家用路由器的全向天线的无线信号分布类似被压扁的椭球形,无线信号在与天线垂直的方向上覆盖效果最好。由于手机的天线位于手机边框内、笔记本电脑的天线位于连接屏幕的水平转轴中…而设备的不同摆放方式也会导致接收信号的效果不同。后面我会证明天线朝同一个方向的叠加摆放与信号的增强没有必然联系,因此正确的摆法应该交叉而不指向同一方向。若是两根天线,则应成直角;若三根,则应如下图所示。


实验结果

为了取得比较可靠的数据,我借鉴了物理实验中交替重复测量的思想,分别进行了不同向、同向、再不同向、再同向…做了共8次实验,结果如下(仅展示其中4次)。




可以看到,两种摆法的延迟和下行速度并没有很大区别(全部竖直向上摆放下行稍快),而上行速度却有显著区别。可见天线朝同一个方向的叠加摆放与信号的增强没有必然联系(有说法说反而会干扰),而朝不同的方向确实利于接收信号,提高上行速度。不过一般我们使用网络大部分都是下行传递数据,总的来说,两种方法各有细微的利弊但在使用上区别不大。


碰到底线咯 后面没有啦

本文标题:signal笔记:路由器天线摆放方法小实验

文章作者:高深远

发布时间:2020年03月12日 - 17:22

最后更新:2020年03月12日 - 18:10

原始链接:https://gsy00517.github.io/signal20200312172247/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
标签: Logisim | 高深远的博客标签: Logisim | 高深远的博客标签: SEO | 高深远的博客标签: SEO | 高深远的博客标签: Vivado | 高深远的博客标签: Vivado | 高深远的博客标签: backbone | 高深远的博客标签: backbone | 高深远的博客标签: hexo | 高深远的博客标签: hexo | 高深远的博客标签 | 高深远的博客
0%
标签 | 高深远的博客
0%
标签: markdown | 高深远的博客标签: markdown | 高深远的博客标签: matlab | 高深远的博客标签: matlab | 高深远的博客标签: python | 高深远的博客标签: python | 高深远的博客标签: pytorch | 高深远的博客标签: pytorch | 高深远的博客标签: ubuntu | 高深远的博客标签: ubuntu | 高深远的博客标签: ubuntu | 高深远的博客标签: ubuntu | 高深远的博客标签: verilog | 高深远的博客标签: verilog | 高深远的博客标签: windows | 高深远的博客标签: windows | 高深远的博客标签: 个人经历 | 高深远的博客标签: 个人经历 | 高深远的博客标签: 代码实现 | 高深远的博客标签: 代码实现 | 高深远的博客标签: 代码实现 | 高深远的博客标签: 代码实现 | 高深远的博客标签: 前端 | 高深远的博客标签: 前端 | 高深远的博客标签: 命令操作 | 高深远的博客标签: 命令操作 | 高深远的博客标签: 命令操作 | 高深远的博客标签: 命令操作 | 高深远的博客标签: 安装教程 | 高深远的博客标签: 安装教程 | 高深远的博客标签: 微积分 | 高深远的博客标签: 微积分 | 高深远的博客标签: 托福 | 高深远的博客标签: 托福 | 高深远的博客标签: 数字电路 | 高深远的博客标签: 数字电路 | 高深远的博客标签: 数据库 | 高深远的博客标签: 数据库 | 高深远的博客标签: 机器学习 | 高深远的博客标签: 机器学习 | 高深远的博客标签: 机器学习 | 高深远的博客标签: 机器学习 | 高深远的博客标签: 模型评估 | 高深远的博客标签: 模型评估 | 高深远的博客标签: 模拟电路 | 高深远的博客标签: 模拟电路 | 高深远的博客标签: 海贼王 | 高深远的博客标签: 海贼王 | 高深远的博客标签: 深度学习 | 高深远的博客标签: 深度学习 | 高深远的博客标签: 深度学习 | 高深远的博客标签: 深度学习 | 高深远的博客标签: 目标检测 | 高深远的博客标签: 目标检测 | 高深远的博客标签: 目标跟踪 | 高深远的博客标签: 目标跟踪 | 高深远的博客标签: 线性代数 | 高深远的博客标签: 线性代数 | 高深远的博客标签: 计算机视觉 | 高深远的博客标签: 计算机视觉 | 高深远的博客标签: 计算机视觉 | 高深远的博客标签: 计算机视觉 | 高深远的博客标签: 论文分享 | 高深远的博客标签: 论文分享 | 高深远的博客标签: 论文分享 | 高深远的博客标签: 论文分享 | 高深远的博客标签: 踩坑血泪 | 高深远的博客标签: 踩坑血泪 | 高深远的博客标签: 踩坑血泪 | 高深远的博客标签: 踩坑血泪 | 高深远的博客标签: 配置优化 | 高深远的博客标签: 配置优化 | 高深远的博客toefl笔记:首考托福——记一次裸考经历 | 高深远的博客

toefl笔记:首考托福——记一次裸考经历

今天上午终于把自己开学以来耿耿于怀的托福考完了。目前来看是铁定要二战了,因此下午抽空把这次考试的经历总结一下,方便再战的时候可以吸取经验和教训。


前期

大一下学期的时候去听了一个学姐的出国分享会,当然是新东方支持的,讲座已结束就被新东方的老师搞了一波传销。当时也不知道托福的有效期是两年,头脑一热就报了班和十月份的托福。不过后来想想可能暑研和暑校也用得上,或许不亏。
于是我抽了个周末去新东方校区做了一个入班小测,很幸运的是我的分能进入强化班,毕竟高中的底子还可以。然而很坑的是,我从同学那得知去年同期的同一个班报名费比我少了将近一千(我报的强化班4400rmb),简直坐地起价啊!可能时间比较早校方不是很担心没人报。
然后到了暑假,我开始零零散散地准备。在家做了一套听力一套阅读,心态就崩了,怎么这么难!感觉就是跟四六级不在一个档次上。

于是三分钟热度就被浇没了,之后就只是背单词了,打算等八月上了课听了老师的解题方法再强化练习。
到了八月中旬,开始上课了,听了几节课明白题型之后,感觉托福或许也没有想象得那么可怕,熟悉就好。那段时间回去后断断续续刷了几套TPO的阅读和听力部分。
然后开学一周,很快就到了国庆。根据我的原计划,身为拖延症重度患者的我打算在国庆力挽狂澜,为此我早早地准备好了新东方的《7天搞定托福高频核心词》,当时觉得时间充裕,计划合理,未来充满希望。然而…

等到国庆结束,我也明白了一个道理:不能被事物的表面现象所迷惑。不过,乐观的我依然觉得剩下的两周足以完成复习。
However,国庆上来劈头盖脸砸过来的课程和任务让我分身乏术。周三晚上的实验课,我做到十一点才回寝室,这更是对我的致命一击。因为回寝太晚,没时间更换被子,导致挨冻一晚上(武汉的天气太怪了)。最后,又是喉咙痛,又是犯鼻炎,那时就感觉托福基本要凉。
考前第二天,我又刷了一套新托福的阅读和听力,成绩不是特别理想。由于新东方的TPO加载速度感人,也可能是学校网络的问题,总之直到考试之前,我只在小站刷过三套TPO,在新东方刷过两套老TPO和一套新TPO。

甚至直到写这篇文章的标题之前,我都不知道托福的英文拼写是“TOEFL”(之前一直觉得是“TOFEL”)。
考试前一天,病情加重,我也就不刷题了,把上课的笔记和题型又好好熟悉了一下就早点休息了。


考前

考前问了新东方替我报名的老师,她说要打印确认信。不知道为什么我无法下载确认信成pdf格式的,最后屏幕截图打算打印,早上去考场的时候却忘记了。不过还好最后发现根本不需要确认信。
早上提早一个小时出寝室,结果发现找不到租八戒了,不知道为什么周六大家起这么早。最后租了辆摩拜拖着病体艰难地骑到了考场。
到了考试的楼下,有一个小姐姐热心地给我指路,不过我马上就发现她别有目的。她让我填一个貌似是培训机构的表格,善良易上当的我稀里糊涂地填了,本来想写个假的电话,结果感冒头很晕也没多想就如实写了,反正我平时也不怎么接陌生电话。
坐电梯到了考试的楼层,碰到我们学院一个经常见到的学长在做志愿者。他总是活跃在各种场合,好像是英语协会的,总之看到他也是开心了一小下。之后就是看序号,签到,领钥匙,去存背包。
在储物室的时候,一边的考试人员一直重复说“A考场的人可以把水拿出来”,我没听太懂。由于之前问过她我鼻炎犯了可不可以带餐巾纸(她说考场会提供),就不想再问第二次了。之前看别人的考试经历,说中途休息时可以出来喝水吃零食,还可以看写作模板,我以为是可以回到储物室的,后来才发现不能。
放完东西,我本来想再看会英语进入一下状态,结果过安检之后就只能一直在里边等了。
我们来到一个签承诺书的房间,大家都一排排坐在一种比较矮的长凳上,我拿了一张承诺书和一支笔就往后坐了。其实还可以拿个写字的时候用来垫的板子,我没注意,不过好多人和我一样都是在腿上或者趴凳子上写的。写承诺书的时候我没仔细看黑板上的要求,写错了一次,只好挺无奈地找工作人员换了一张。
不一会人就好多了,我发现这次考托福的女生比较多,大约是男生的两倍,这在我们学校很不常见啦。考试的也有大人,在我观察是不是还有培训机构的老师的时候,我的隔壁也是实验班的一个朋友也来考试了。他在C考场,那个考场更大。我的A考场人最少,相对来说环境要理想一些。不过不同考场的同学还是在同一个房间等待的。我继续观察,发现还是有几个大二面孔的,和几个人说了几句,发现还是有不少首考的人的。


入场

过了一会有一个男老师进来说一些有关考试的注意事项,说完没多久大家就到隔壁的考试教室刷脸入场了。
考场的教室和等候的教室一样,也是黄色的日光灯,看着也挺舒适。入场顺序是按照姓氏的首字母顺序的,我进去的比较早。尽管A考场大概也就二三十个人,但整个入场过程还是挺久的。
考官把我领到座位上,虽然是随机抽的但好像我的考位还是我的序号。为我把身份证插在旁边的卡槽里之后,考官又为我输了激活码进入考试界面,然后没说什么就走了。
考试的隔间挺好,靠桌子往里坐一点就完全看不到旁边了。首先是确认姓名的界面,然而考官走了我也没处问,担心确认了就直接开始考试了因此久久没敢点。由于别人还在入场,因此我不敢太早开始考试。我回头看了一眼,发现是我进候考室以来就注意到的那个男生。虽然没问过他,但看上去他这次绝对不是一战了。
过了一会,我听到有人点鼠标的声音,于是我也鼓足勇气开始点。前面大概有七八的页面都是只需continue的direction界面,而且这个界面是不会自动跳转的,我在这里停留了很久。
终于,大概第十个人入场的时候,我听到有人开始试音了。意外的是,第一个开始试音的人居然真的在介绍他生活的城市。哈哈哈看来也是首考的,不知道待会整个考场一齐开始诠释人类的本质的时候,他有什么感想。这里我暗暗庆幸自己报了班。
当大家都在诠释人类的本质时,我心里觉得还是挺可乐的。不过就在这时,我听到前面提到的那位久经沙场的老哥也开始复读了,于是我又点了一个continue。
每个continue我的拖好久才点,不过入场真的是挺久的。大概有十个人完成试音之后,我才看到了describe the city you live in,心想这个时间还是可以的。


阅读

直到考试,我才知道review的用法,点击之后,会出现一个表格,可以看到哪些题已经answered了。
阅读第一篇是关于研究者根据化石推断远古的自然环境,第二篇是什么记不太清了,好像也是历史相关的,第三篇是北美西海岸土著人的一些文化,还有配图。没有遇到加试。
总的来说,考场里效率还是比寝室要好一些的。我大概还有40分钟时做完了第一篇,到最后一篇时时间还有20多分钟,相对来说还是比较宽裕的。


听力

接下来就是听力了,我的听力是加试,有3个section。第一个section的对话我考虑太久,导致最后答lecture三道题要在一分钟之内答完。当时也只能以这个section只有50%的概率计入成绩来安慰自己。
第二个section做得还行,一些笔记还是没记到要点上,还是得多练。遗憾的是,我听力有好几篇都没听懂听力开头“you will hear part of a lecture in a ……gy class”中学科具体是什么,如果能听懂的话肯定是有一定帮助的,词汇量还不够啊!

到了第三个section,我之前在考试教室门外抽的餐巾纸用完了,我只能忍着做题,结果第三个section的lecture的conversation中的男老师似乎有异国口音,说得很不清楚,在lecture部分我也走了一会神。同样的,我发现我不是很善于掌握1个conversation+1个lecture情况下的时间,lecture的时间又分配得不多。当时也只能又以这个section只有50%的概率计入成绩来安慰自己,好吧其实两个section都凉了。
考试前两天对自己的listening还是最自信的,现在看来还是得花真功夫才行。


休息

终于休息了,我出门的时候拿到张纸条,上面提醒我11:04返场。我本来想喝口热水缓解一下我喉咙的疼痛,却被告知不能回储物室了。我这时候看到别的同学放在楼梯口的一个大桌子上的水和零食,心里真的拔凉拔凉的。我5块钱买的士力架啊!我的口语写作模板啊!不过好像大多数人都没怎么吃东西,要是我不生病的话应该也没什么问题。
考场外面的钟不是很准,我一直担心里面开始口语了我还没进去,后来发现开始第二部分的考试也是需要考官输入验证码的,因此完全不必担心。
什么都没带,我那十分钟也就上了个厕所并且补充了餐巾纸。


口语

之前开始的晚,因此我休息的时间也差不多在大家的中间。口语部分一开始的continue就比较少了,又试了一次音。这回就没有人真正介绍自己的城市了,大家又当了一回复读机。可能由于感冒造成鼻音太重的问题,我试音的音量偏低,得说得用点劲才行。
我在这里也停顿得有点久,因为待会等大家都开始说了,我就可以偷偷混投入其中以掩盖我拙劣的口语哈哈。事实上,和大家一起说真的能说得更开更自信,当大部分人说完之后,我们考场里有一个女生还在说,我明显地听到她顿了一下,然后声音顿时小了很多。
之前超牛的老哥老早就进去了(他很早就完成了听力),我进去之后本想偷听他在说什么,因为口语题都一样,结果…天呐竟然跟不上他的语速!还好这时候有一个水平不高但的确可以帮到我的吞吞吐吐的小哥开始讲了,我听到他在说work什么的,自己在脑子里构思了一下便也开始答题了。
然而,我的提前构思反倒先入为主了。当题目放出来时,我花了好久才读清题目,因为跟我想的太不一样了。以后还是不能太期望于听到别人的答案。最后,第一部分比较凉。
其实整个口语都比较凉,因为我感冒鼻音简直太重了,就像蒙着几个口罩一样,特别是其中有个录音我还咳嗽了几声。


写作

最后两部分考试感觉时间飞快,既然口语凉了,也听说写作给分还可以,我就放飞自我开始写了。
在我听听力的时候,就听到劈里啪啦的打字声了。一直对自己打字速度很有自信的我还是小惊讶了一下。
两篇作文都不是很难,我第二篇大概只写了三百词出头一些,细节还是写的有点少,都是论述的,这点下回要改进。两篇作文都是到点自动保存提交的,没有整体检查拼写,打字速度还是得练,盲打还是得加强。考场的机械键盘相对来说比较扁平,跟笔记本手感还是比较相近的。


考完

最后还有一个report成绩还是cancel的选项,考官明确说过这个不能提问,于是我看的很仔细。还好我的水平还是无压力看懂了,砸了两千块当然要report啦!出考场后还是有点不放心特地查了一下,发现还是有网友选cancel的,不过好像可以付额外的费用解决。
出考场才知道已经十二点半了,由于中途没补充零食,肚子也是饿得咕咕叫(写大作文的时候开始明显感到饿)。从储物柜拿手机的时候,不小心带了出来,掉在地上了,心里一惊,还好只在钢化膜上留下一条线,当时也觉得无所谓了。
考完也挺平静的,感觉托福考试也就这样,只可惜这次时运不济,命途多舛。以后考托福一定要在学期初考,并且好好准备,关键是要注意身体的健康!
早上起来的时候百度了一下自己的博客,发现已经被李彦宏爸爸的百度收录了,可以直接百度到我的博客和文章,也是今天比较开心的一件事吧,以后会好好做SEO的。


结果

出成绩了,更新一下。
10月19号上午考完的试,31号凌晨3:51终于刷新的成绩。查询页面显示的是“2019年10月23日的MyBest Scores”,不知道是不是23号就批好了成绩。考完后一直关注贴吧和公众号,似乎我那周是最后一次最多两周出成绩的考试,以后托福的成绩好像都会考后10天就出结果。雅思更狠,马上跟着改成了6天出成绩,它们是不是也在竞争呢…
查分的时候还是很忐忑的,没想到这次首考的成绩能到90+,虽然完全不够,但还是比我想象得要好一些的。口语果然离20还是差了一点,或许有生病的影响,但的确能体现我的水平,还是得加强练习!别人口中的提分项——写作,我也没有取得高分,看来还是不能大意,平时需要熟能生巧。但愿二战能够取得一定的进步吧!
今天去听了我们学校的海外交流项目介绍的讲座,大体上的语言成绩要求是CET4>550,CET6>500,TOEFL>80,IELTS>6.0,否则要电话面试,但这些基本都是相对来说比较中规中矩的科研项目或者学分项目,还是得努力提高英语水平啊!


碰到底线咯 后面没有啦

本文标题:toefl笔记:首考托福——记一次裸考经历

文章作者:高深远

发布时间:2019年10月19日 - 15:04

最后更新:2020年02月05日 - 09:59

原始链接:https://gsy00517.github.io/toefl20191019150438/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
toefl笔记:首考托福——记一次裸考经历 | 高深远的博客

toefl笔记:首考托福——记一次裸考经历

今天上午终于把自己开学以来耿耿于怀的托福考完了。目前来看是铁定要二战了,因此下午抽空把这次考试的经历总结一下,方便再战的时候可以吸取经验和教训。


前期

大一下学期的时候去听了一个学姐的出国分享会,当然是新东方支持的,讲座已结束就被新东方的老师搞了一波传销。当时也不知道托福的有效期是两年,头脑一热就报了班和十月份的托福。不过后来想想可能暑研和暑校也用得上,或许不亏。
于是我抽了个周末去新东方校区做了一个入班小测,很幸运的是我的分能进入强化班,毕竟高中的底子还可以。然而很坑的是,我从同学那得知去年同期的同一个班报名费比我少了将近一千(我报的强化班4400rmb),简直坐地起价啊!可能时间比较早校方不是很担心没人报。
然后到了暑假,我开始零零散散地准备。在家做了一套听力一套阅读,心态就崩了,怎么这么难!感觉就是跟四六级不在一个档次上。

于是三分钟热度就被浇没了,之后就只是背单词了,打算等八月上了课听了老师的解题方法再强化练习。
到了八月中旬,开始上课了,听了几节课明白题型之后,感觉托福或许也没有想象得那么可怕,熟悉就好。那段时间回去后断断续续刷了几套TPO的阅读和听力部分。
然后开学一周,很快就到了国庆。根据我的原计划,身为拖延症重度患者的我打算在国庆力挽狂澜,为此我早早地准备好了新东方的《7天搞定托福高频核心词》,当时觉得时间充裕,计划合理,未来充满希望。然而…

等到国庆结束,我也明白了一个道理:不能被事物的表面现象所迷惑。不过,乐观的我依然觉得剩下的两周足以完成复习。
However,国庆上来劈头盖脸砸过来的课程和任务让我分身乏术。周三晚上的实验课,我做到十一点才回寝室,这更是对我的致命一击。因为回寝太晚,没时间更换被子,导致挨冻一晚上(武汉的天气太怪了)。最后,又是喉咙痛,又是犯鼻炎,那时就感觉托福基本要凉。
考前第二天,我又刷了一套新托福的阅读和听力,成绩不是特别理想。由于新东方的TPO加载速度感人,也可能是学校网络的问题,总之直到考试之前,我只在小站刷过三套TPO,在新东方刷过两套老TPO和一套新TPO。

甚至直到写这篇文章的标题之前,我都不知道托福的英文拼写是“TOEFL”(之前一直觉得是“TOFEL”)。
考试前一天,病情加重,我也就不刷题了,把上课的笔记和题型又好好熟悉了一下就早点休息了。


考前

考前问了新东方替我报名的老师,她说要打印确认信。不知道为什么我无法下载确认信成pdf格式的,最后屏幕截图打算打印,早上去考场的时候却忘记了。不过还好最后发现根本不需要确认信。
早上提早一个小时出寝室,结果发现找不到租八戒了,不知道为什么周六大家起这么早。最后租了辆摩拜拖着病体艰难地骑到了考场。
到了考试的楼下,有一个小姐姐热心地给我指路,不过我马上就发现她别有目的。她让我填一个貌似是培训机构的表格,善良易上当的我稀里糊涂地填了,本来想写个假的电话,结果感冒头很晕也没多想就如实写了,反正我平时也不怎么接陌生电话。
坐电梯到了考试的楼层,碰到我们学院一个经常见到的学长在做志愿者。他总是活跃在各种场合,好像是英语协会的,总之看到他也是开心了一小下。之后就是看序号,签到,领钥匙,去存背包。
在储物室的时候,一边的考试人员一直重复说“A考场的人可以把水拿出来”,我没听太懂。由于之前问过她我鼻炎犯了可不可以带餐巾纸(她说考场会提供),就不想再问第二次了。之前看别人的考试经历,说中途休息时可以出来喝水吃零食,还可以看写作模板,我以为是可以回到储物室的,后来才发现不能。
放完东西,我本来想再看会英语进入一下状态,结果过安检之后就只能一直在里边等了。
我们来到一个签承诺书的房间,大家都一排排坐在一种比较矮的长凳上,我拿了一张承诺书和一支笔就往后坐了。其实还可以拿个写字的时候用来垫的板子,我没注意,不过好多人和我一样都是在腿上或者趴凳子上写的。写承诺书的时候我没仔细看黑板上的要求,写错了一次,只好挺无奈地找工作人员换了一张。
不一会人就好多了,我发现这次考托福的女生比较多,大约是男生的两倍,这在我们学校很不常见啦。考试的也有大人,在我观察是不是还有培训机构的老师的时候,我的隔壁也是实验班的一个朋友也来考试了。他在C考场,那个考场更大。我的A考场人最少,相对来说环境要理想一些。不过不同考场的同学还是在同一个房间等待的。我继续观察,发现还是有几个大二面孔的,和几个人说了几句,发现还是有不少首考的人的。


入场

过了一会有一个男老师进来说一些有关考试的注意事项,说完没多久大家就到隔壁的考试教室刷脸入场了。
考场的教室和等候的教室一样,也是黄色的日光灯,看着也挺舒适。入场顺序是按照姓氏的首字母顺序的,我进去的比较早。尽管A考场大概也就二三十个人,但整个入场过程还是挺久的。
考官把我领到座位上,虽然是随机抽的但好像我的考位还是我的序号。为我把身份证插在旁边的卡槽里之后,考官又为我输了激活码进入考试界面,然后没说什么就走了。
考试的隔间挺好,靠桌子往里坐一点就完全看不到旁边了。首先是确认姓名的界面,然而考官走了我也没处问,担心确认了就直接开始考试了因此久久没敢点。由于别人还在入场,因此我不敢太早开始考试。我回头看了一眼,发现是我进候考室以来就注意到的那个男生。虽然没问过他,但看上去他这次绝对不是一战了。
过了一会,我听到有人点鼠标的声音,于是我也鼓足勇气开始点。前面大概有七八的页面都是只需continue的direction界面,而且这个界面是不会自动跳转的,我在这里停留了很久。
终于,大概第十个人入场的时候,我听到有人开始试音了。意外的是,第一个开始试音的人居然真的在介绍他生活的城市。哈哈哈看来也是首考的,不知道待会整个考场一齐开始诠释人类的本质的时候,他有什么感想。这里我暗暗庆幸自己报了班。
当大家都在诠释人类的本质时,我心里觉得还是挺可乐的。不过就在这时,我听到前面提到的那位久经沙场的老哥也开始复读了,于是我又点了一个continue。
每个continue我的拖好久才点,不过入场真的是挺久的。大概有十个人完成试音之后,我才看到了describe the city you live in,心想这个时间还是可以的。


阅读

直到考试,我才知道review的用法,点击之后,会出现一个表格,可以看到哪些题已经answered了。
阅读第一篇是关于研究者根据化石推断远古的自然环境,第二篇是什么记不太清了,好像也是历史相关的,第三篇是北美西海岸土著人的一些文化,还有配图。没有遇到加试。
总的来说,考场里效率还是比寝室要好一些的。我大概还有40分钟时做完了第一篇,到最后一篇时时间还有20多分钟,相对来说还是比较宽裕的。


听力

接下来就是听力了,我的听力是加试,有3个section。第一个section的对话我考虑太久,导致最后答lecture三道题要在一分钟之内答完。当时也只能以这个section只有50%的概率计入成绩来安慰自己。
第二个section做得还行,一些笔记还是没记到要点上,还是得多练。遗憾的是,我听力有好几篇都没听懂听力开头“you will hear part of a lecture in a ……gy class”中学科具体是什么,如果能听懂的话肯定是有一定帮助的,词汇量还不够啊!

到了第三个section,我之前在考试教室门外抽的餐巾纸用完了,我只能忍着做题,结果第三个section的lecture的conversation中的男老师似乎有异国口音,说得很不清楚,在lecture部分我也走了一会神。同样的,我发现我不是很善于掌握1个conversation+1个lecture情况下的时间,lecture的时间又分配得不多。当时也只能又以这个section只有50%的概率计入成绩来安慰自己,好吧其实两个section都凉了。
考试前两天对自己的listening还是最自信的,现在看来还是得花真功夫才行。


休息

终于休息了,我出门的时候拿到张纸条,上面提醒我11:04返场。我本来想喝口热水缓解一下我喉咙的疼痛,却被告知不能回储物室了。我这时候看到别的同学放在楼梯口的一个大桌子上的水和零食,心里真的拔凉拔凉的。我5块钱买的士力架啊!我的口语写作模板啊!不过好像大多数人都没怎么吃东西,要是我不生病的话应该也没什么问题。
考场外面的钟不是很准,我一直担心里面开始口语了我还没进去,后来发现开始第二部分的考试也是需要考官输入验证码的,因此完全不必担心。
什么都没带,我那十分钟也就上了个厕所并且补充了餐巾纸。


口语

之前开始的晚,因此我休息的时间也差不多在大家的中间。口语部分一开始的continue就比较少了,又试了一次音。这回就没有人真正介绍自己的城市了,大家又当了一回复读机。可能由于感冒造成鼻音太重的问题,我试音的音量偏低,得说得用点劲才行。
我在这里也停顿得有点久,因为待会等大家都开始说了,我就可以偷偷混投入其中以掩盖我拙劣的口语哈哈。事实上,和大家一起说真的能说得更开更自信,当大部分人说完之后,我们考场里有一个女生还在说,我明显地听到她顿了一下,然后声音顿时小了很多。
之前超牛的老哥老早就进去了(他很早就完成了听力),我进去之后本想偷听他在说什么,因为口语题都一样,结果…天呐竟然跟不上他的语速!还好这时候有一个水平不高但的确可以帮到我的吞吞吐吐的小哥开始讲了,我听到他在说work什么的,自己在脑子里构思了一下便也开始答题了。
然而,我的提前构思反倒先入为主了。当题目放出来时,我花了好久才读清题目,因为跟我想的太不一样了。以后还是不能太期望于听到别人的答案。最后,第一部分比较凉。
其实整个口语都比较凉,因为我感冒鼻音简直太重了,就像蒙着几个口罩一样,特别是其中有个录音我还咳嗽了几声。


写作

最后两部分考试感觉时间飞快,既然口语凉了,也听说写作给分还可以,我就放飞自我开始写了。
在我听听力的时候,就听到劈里啪啦的打字声了。一直对自己打字速度很有自信的我还是小惊讶了一下。
两篇作文都不是很难,我第二篇大概只写了三百词出头一些,细节还是写的有点少,都是论述的,这点下回要改进。两篇作文都是到点自动保存提交的,没有整体检查拼写,打字速度还是得练,盲打还是得加强。考场的机械键盘相对来说比较扁平,跟笔记本手感还是比较相近的。


考完

最后还有一个report成绩还是cancel的选项,考官明确说过这个不能提问,于是我看的很仔细。还好我的水平还是无压力看懂了,砸了两千块当然要report啦!出考场后还是有点不放心特地查了一下,发现还是有网友选cancel的,不过好像可以付额外的费用解决。
出考场才知道已经十二点半了,由于中途没补充零食,肚子也是饿得咕咕叫(写大作文的时候开始明显感到饿)。从储物柜拿手机的时候,不小心带了出来,掉在地上了,心里一惊,还好只在钢化膜上留下一条线,当时也觉得无所谓了。
考完也挺平静的,感觉托福考试也就这样,只可惜这次时运不济,命途多舛。以后考托福一定要在学期初考,并且好好准备,关键是要注意身体的健康!
早上起来的时候百度了一下自己的博客,发现已经被李彦宏爸爸的百度收录了,可以直接百度到我的博客和文章,也是今天比较开心的一件事吧,以后会好好做SEO的。


结果

出成绩了,更新一下。
10月19号上午考完的试,31号凌晨3:51终于刷新的成绩。查询页面显示的是“2019年10月23日的MyBest Scores”,不知道是不是23号就批好了成绩。考完后一直关注贴吧和公众号,似乎我那周是最后一次最多两周出成绩的考试,以后托福的成绩好像都会考后10天就出结果。雅思更狠,马上跟着改成了6天出成绩,它们是不是也在竞争呢…
查分的时候还是很忐忑的,没想到这次首考的成绩能到90+,虽然完全不够,但还是比我想象得要好一些的。口语果然离20还是差了一点,或许有生病的影响,但的确能体现我的水平,还是得加强练习!别人口中的提分项——写作,我也没有取得高分,看来还是不能大意,平时需要熟能生巧。但愿二战能够取得一定的进步吧!
今天去听了我们学校的海外交流项目介绍的讲座,大体上的语言成绩要求是CET4>550,CET6>500,TOEFL>80,IELTS>6.0,否则要电话面试,但这些基本都是相对来说比较中规中矩的科研项目或者学分项目,还是得努力提高英语水平啊!


碰到底线咯 后面没有啦

本文标题:toefl笔记:首考托福——记一次裸考经历

文章作者:高深远

发布时间:2019年10月19日 - 15:04

最后更新:2020年02月05日 - 09:59

原始链接:https://gsy00517.github.io/toefl20191019150438/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:释放空间 | 高深远的博客

ubuntu笔记:释放空间

前一篇讲了如何清理windows下的空间,然而虽然ubuntu中垃圾文件没win10那么多,可是我给ubuntu分配的空间比win10少得多了,于是我又找了些清理ubuntu的方法。

References

电子文献:
https://www.jb51.net/article/164589.htm
https://blog.csdn.net/m0_37407756/article/details/79903837


查看

我们可以在终端中使用df命令来查看磁盘的利用情况。
另外,可以加一个-h即“human reading”使显示的磁盘利用状况列表更加适合我们阅读(主要是转化了单位和列名)。


方法

  1. 清理apt缓存文件

    ubuntu在/var/cache/apt/archives目录中会保留deb软件包的缓冲文件。随着时间的推移,这些缓存可能会占有很多空间。
    我们可以使用sudo du -sh /var/cache/apt来查看当前apt缓存文件的占用的大小。
    我们可以直接在终端执行如下命令以清理过时的软件包:

    1
    sudo apt-get autoclean

    我们可以在终端中执行以下命令来移除所有apt缓存中的软件包:

    1
    sudo apt-get clean

    实践证明,这两条命令其实清理得不是非常干净(会剩下kb级的缓存),不过如果很久没清理的话,还是非常强力的。

  2. 删除其他软件依赖的但现在已不用的软件包

    下面这条命令可以移除系统不再需要的依赖库和软件包。这些软件包是自动安装的,是当初为了使得某个安装的软件包满足依赖关系,而此时已不再需要。

    1
    sudo apt-get autoremove

    除了移除不再被系统需要的孤立软件包,这条命令也会移除安装在系统中的linux旧内核(有更精确的操作方法,有点专业,这里就不说了)。
    注意,这条命令执行后,软件的配置文件还是会保留的。
    可以使用purge选项来同时清除软件包和软件的配置文件。

    1
    sudo apt-get autoremove --purge

    补充:这里扯点题外话,最近看到一个挺好用的命令apt-get install -f,其作用是修复依赖关系(depends),即假如系统上有某个package不满足依赖条件,这个命令就会自动安装那个package所依赖的package。

  3. 清除缩略图缓存

    可以使用du -sh ~/.cache/thumbnails/查看缩略图缓存占用的空间。
    其实如果不是摄影爱好者或者类似的使用者的话,这个缓存不会特别大,不过对缓存强迫症患者还是可以清一下的。

    1
    rm -rf ~/.cache/thumbnails/*
  4. 清除残余配置文件

    可以使用dpkg --list | grep "^rc"查看残余的配置文件,如果没有的话,可以跳过后文。
    这里的rc表示软件包已经删除(Remove),但配置文件(Config-file)还在的文件。
    这里具体的介绍可以看一下我新写的文章ubuntu笔记:安装与卸载deb软件包
    若有,咱们来删除:

    1
    dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P

    或者

    1
    dpkg --list | grep "^rc" | cut -d " " -f 3 | xargs sudo dpkg --purge

    这时候如果出现如下错误,那无需担心,因为已经不存在残余的配置文件了。

可以把上面的命令按顺序执行一遍,就完成了对ubuntu系统的空间释放。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:释放空间

文章作者:高深远

发布时间:2019年09月14日 - 09:48

最后更新:2020年01月26日 - 10:02

原始链接:https://gsy00517.github.io/ubuntu20190914094853/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:释放空间 | 高深远的博客

ubuntu笔记:释放空间

前一篇讲了如何清理windows下的空间,然而虽然ubuntu中垃圾文件没win10那么多,可是我给ubuntu分配的空间比win10少得多了,于是我又找了些清理ubuntu的方法。

References

电子文献:
https://www.jb51.net/article/164589.htm
https://blog.csdn.net/m0_37407756/article/details/79903837


查看

我们可以在终端中使用df命令来查看磁盘的利用情况。
另外,可以加一个-h即“human reading”使显示的磁盘利用状况列表更加适合我们阅读(主要是转化了单位和列名)。


方法

  1. 清理apt缓存文件

    ubuntu在/var/cache/apt/archives目录中会保留deb软件包的缓冲文件。随着时间的推移,这些缓存可能会占有很多空间。
    我们可以使用sudo du -sh /var/cache/apt来查看当前apt缓存文件的占用的大小。
    我们可以直接在终端执行如下命令以清理过时的软件包:

    1
    sudo apt-get autoclean

    我们可以在终端中执行以下命令来移除所有apt缓存中的软件包:

    1
    sudo apt-get clean

    实践证明,这两条命令其实清理得不是非常干净(会剩下kb级的缓存),不过如果很久没清理的话,还是非常强力的。

  2. 删除其他软件依赖的但现在已不用的软件包

    下面这条命令可以移除系统不再需要的依赖库和软件包。这些软件包是自动安装的,是当初为了使得某个安装的软件包满足依赖关系,而此时已不再需要。

    1
    sudo apt-get autoremove

    除了移除不再被系统需要的孤立软件包,这条命令也会移除安装在系统中的linux旧内核(有更精确的操作方法,有点专业,这里就不说了)。
    注意,这条命令执行后,软件的配置文件还是会保留的。
    可以使用purge选项来同时清除软件包和软件的配置文件。

    1
    sudo apt-get autoremove --purge

    补充:这里扯点题外话,最近看到一个挺好用的命令apt-get install -f,其作用是修复依赖关系(depends),即假如系统上有某个package不满足依赖条件,这个命令就会自动安装那个package所依赖的package。

  3. 清除缩略图缓存

    可以使用du -sh ~/.cache/thumbnails/查看缩略图缓存占用的空间。
    其实如果不是摄影爱好者或者类似的使用者的话,这个缓存不会特别大,不过对缓存强迫症患者还是可以清一下的。

    1
    rm -rf ~/.cache/thumbnails/*
  4. 清除残余配置文件

    可以使用dpkg --list | grep "^rc"查看残余的配置文件,如果没有的话,可以跳过后文。
    这里的rc表示软件包已经删除(Remove),但配置文件(Config-file)还在的文件。
    这里具体的介绍可以看一下我新写的文章ubuntu笔记:安装与卸载deb软件包
    若有,咱们来删除:

    1
    dpkg -l |grep ^rc|awk '{print $2}' |sudo xargs dpkg -P

    或者

    1
    dpkg --list | grep "^rc" | cut -d " " -f 3 | xargs sudo dpkg --purge

    这时候如果出现如下错误,那无需担心,因为已经不存在残余的配置文件了。

可以把上面的命令按顺序执行一遍,就完成了对ubuntu系统的空间释放。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:释放空间

文章作者:高深远

发布时间:2019年09月14日 - 09:48

最后更新:2020年01月26日 - 10:02

原始链接:https://gsy00517.github.io/ubuntu20190914094853/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:重装ubuntu——记一段辛酸血泪史 | 高深远的博客

ubuntu笔记:重装ubuntu——记一段辛酸血泪史

这是不久前我踩过的一个巨坑,在这里我想先强调一下:
不要升级linux发行版!!!重要的事情说三遍!!!
不要升级linux发行版!!!重要的事情说三遍!!!
不要升级linux发行版!!!重要的事情说三遍!!!
为什么?不要问我为什么。我按系统提示升级ubuntu到18.04LTS后,就再也进不去系统了。不信你可以尝试一下,你将会看到如下界面:

注:图片来自网络,我就不再为了截图而踩一次坑了。

不仅是图形界面,命令行界面也进不去了(据说可以在重启时选择recovery mode并且狂按回车强行进入界面,但我失败了)。
不过如果你真的尝试了并且掉坑里了的话,没关系,你获得了一个很好的重装系统的锻炼机会。下面我们就按步骤锻炼一下。
截止本文最后一次更新前,我已用此方法安装过3遍系统(2遍16.04LTS,1遍18.04LTS),可以放心食用。由于我目前使用的是18.04LTS版本,而ubuntu18.04的社区也日渐活跃,因此本文将会涉及一些18.04版的安装步骤,基本上是一致的。

注:LTS表示的是长期支持版本,一般.04都是LTS的,每两年发布一次,支持期好像是5年。

本文同时适用于ubuntu16.04LTS和ubuntu18.04LTS。

References

电子文献:
https://blog.csdn.net/Spacegene/article/details/86659349


准备

  1. U盘

    准备一个2G以上的无用的U盘,或者备份好里面的文件。然后将其格式化。
  2. 镜像

    下载ubuntu16.04LTS镜像或者ubuntu18.04LTS镜像到本地。也可使用清华源镜像或者阿里云镜像更快下载。
  3. 删除分区

    你可以通过控制面板中的创建并格式化硬盘分区来看到你的windows与ubuntu分区的情况。(以下操作都是针对重装,不再重新分区,需要的可以自行上网查找教程)
    在重新安装ubuntu16.04之前我们需要删除原先Ubuntu的EFI分区及启动引导项,这里推荐直接使用windows下的diskpart来删除。
    使用win+R输入diskpart打开diskpart.exe,允许其对设备进行更改。

    接下来使用list dick,我的笔记本当时只有一块SSD,两个系统都装在上面,故select disk 0进入disk 0。然后就可以输入list partition来查看具体的分区信息。

    其中类型未知的便是分给ubuntu的分区,我这里有一块8G的swap分区和60G的/分区。
    接下来执行如下命令:

    1
    2
    3
    4
    select partition 7
    delete partition override #删除该分区
    select partition 8
    delete partition override #删除该分区

    注意:以上命令是针对我的情况,具体请按照对应ubuntu分区的序号删除。

    现在你可以在控制面板中的创建并格式化硬盘分区中看到你删除的分区已经合并成一块未分配的空间,这也意味着你与你原来ubuntu上的数据彻底说再见了。

  4. 删除ubuntu启动引导项

    首先下载EasyUEFI,使用免费试用版EasyUEFI Trial即可。如果试用期过了的话可以到网上找破解版来下。
    下载完成后安装,打开EasyUEFI如图: 选择管理EFI启动选项Manage EFI Boot Option,然后选择ubuntu启动引导项,点击中间的删除按钮来删除该引导项。 现在重新启动,你会发现已经没有让你选择系统的引导界面,而是直接进入windows系统。
  5. 制作启动U盘

    首先我们下载一个免费的U盘制作工具rufus
    此时插入已经格式化的U盘,打开rufus,一般情况下它会自动选择U盘,你也可以在device选项下手动选择或确认。
    点击select,选择之前下载好的镜像文件。
    其他设置保留默认即可,不放心的话可以比对下图: 然后start开始制作。如果此时rufus提示需要下载一些其它文件,选择Yes继续即可。
    没有问题的话制作完的U盘会如图所示: 在下面的步骤中,请一直插着U盘不要拔。

安装

现在重新启动电脑,开机的过程中不停地快按F12进入bios界面(我的是戴尔的电脑,不同电脑按键或有不同,自行百度;如果快按不行的话再次重启试一试长按,因为网上有些教程说的是长按,而我是长按不行而快按可以)。
随后选择U盘启动(不同电脑这个界面也可能不一样,具体可以百度,我选择的是UEFI BOOT中UEFI:U盘名那项)。
接下来就进入了紫红色的GNU GRUB界面,选择install ubuntu。如果在这里迟疑了一下,会自动进入trying without install,这也没关系,你也可以进入后在图形界面中双击安装,安装之后可以继续试用,直到重启。
随后就是些比较简单的安装过程,基本上可以按默认进行,因为是重装,好像不需要联网安装且有汉化包。

注意:若是安装18.04LTS,这里会出现一个“正常安装”还是“最小安装”的选择,一般无脑选择正常安装即可,但请事先对安装时间做好心里准备(我大概花了一个多小时)且安装完后有些软件还是比较多余的,可以手动卸载。

接下来是比较重要的部分:
进入安装类型installation type界面后,选择其他选项something else,这样我们就可以自己配置ubuntu分区了,点击继续。
接下来会进入一个磁盘分区的界面,选中之前清出来的未分配分区(名为“空闲”,也可以通过大小来判断),点击下方+号,新建一个swap交换分区,大小为8G左右(一般和电脑的内存相当即可,不分这个区会有警告,也可不分之后再加)。
再次双击空闲分区,挂载点下拉,选择/(相当于windows的C盘)。
在安装启动引导器的设备选项中,选择Windows Boot Manager。
结果可以参考下图:

确认无误后点击现在安装,然后就一路默认直到安装完成。
这里会有一个设置用户名和计算机名的界面,建议设置得短一些比较好,否则在终端中每条键入的命令前都会有很长的一串“用户名@计算机名”。
安装完成后可以进行试用,此时一切操作都不会被保留。
如果无需试用,就重新启动系统,此时会提醒可以拔出installation medium即启动U盘。


后期

别忘了把U盘格式化回来,可以继续使用,留着做纪念也行,说不定哪天又要重装。
下面我展示一下我目前的一部分美化效果,亲测发现这只会牺牲一点点儿CPU,所以并不用担心,大胆地美化就是,可能这也是使用linux发行版不多的几种乐趣之一吧。





以上就是重装ubuntu的全部内容,欢迎补充!我也会在新问题出现时及时更新。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:重装ubuntu——记一段辛酸血泪史

文章作者:高深远

发布时间:2019年09月14日 - 10:00

最后更新:2020年02月07日 - 16:58

原始链接:https://gsy00517.github.io/ubuntu20190914100050/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:重装ubuntu——记一段辛酸血泪史 | 高深远的博客

ubuntu笔记:重装ubuntu——记一段辛酸血泪史

这是不久前我踩过的一个巨坑,在这里我想先强调一下:
不要升级linux发行版!!!重要的事情说三遍!!!
不要升级linux发行版!!!重要的事情说三遍!!!
不要升级linux发行版!!!重要的事情说三遍!!!
为什么?不要问我为什么。我按系统提示升级ubuntu到18.04LTS后,就再也进不去系统了。不信你可以尝试一下,你将会看到如下界面:

注:图片来自网络,我就不再为了截图而踩一次坑了。

不仅是图形界面,命令行界面也进不去了(据说可以在重启时选择recovery mode并且狂按回车强行进入界面,但我失败了)。
不过如果你真的尝试了并且掉坑里了的话,没关系,你获得了一个很好的重装系统的锻炼机会。下面我们就按步骤锻炼一下。
截止本文最后一次更新前,我已用此方法安装过3遍系统(2遍16.04LTS,1遍18.04LTS),可以放心食用。由于我目前使用的是18.04LTS版本,而ubuntu18.04的社区也日渐活跃,因此本文将会涉及一些18.04版的安装步骤,基本上是一致的。

注:LTS表示的是长期支持版本,一般.04都是LTS的,每两年发布一次,支持期好像是5年。

本文同时适用于ubuntu16.04LTS和ubuntu18.04LTS。

References

电子文献:
https://blog.csdn.net/Spacegene/article/details/86659349


准备

  1. U盘

    准备一个2G以上的无用的U盘,或者备份好里面的文件。然后将其格式化。
  2. 镜像

    下载ubuntu16.04LTS镜像或者ubuntu18.04LTS镜像到本地。也可使用清华源镜像或者阿里云镜像更快下载。
  3. 删除分区

    你可以通过控制面板中的创建并格式化硬盘分区来看到你的windows与ubuntu分区的情况。(以下操作都是针对重装,不再重新分区,需要的可以自行上网查找教程)
    在重新安装ubuntu16.04之前我们需要删除原先Ubuntu的EFI分区及启动引导项,这里推荐直接使用windows下的diskpart来删除。
    使用win+R输入diskpart打开diskpart.exe,允许其对设备进行更改。

    接下来使用list dick,我的笔记本当时只有一块SSD,两个系统都装在上面,故select disk 0进入disk 0。然后就可以输入list partition来查看具体的分区信息。

    其中类型未知的便是分给ubuntu的分区,我这里有一块8G的swap分区和60G的/分区。
    接下来执行如下命令:

    1
    2
    3
    4
    select partition 7
    delete partition override #删除该分区
    select partition 8
    delete partition override #删除该分区

    注意:以上命令是针对我的情况,具体请按照对应ubuntu分区的序号删除。

    现在你可以在控制面板中的创建并格式化硬盘分区中看到你删除的分区已经合并成一块未分配的空间,这也意味着你与你原来ubuntu上的数据彻底说再见了。

  4. 删除ubuntu启动引导项

    首先下载EasyUEFI,使用免费试用版EasyUEFI Trial即可。如果试用期过了的话可以到网上找破解版来下。
    下载完成后安装,打开EasyUEFI如图: 选择管理EFI启动选项Manage EFI Boot Option,然后选择ubuntu启动引导项,点击中间的删除按钮来删除该引导项。 现在重新启动,你会发现已经没有让你选择系统的引导界面,而是直接进入windows系统。
  5. 制作启动U盘

    首先我们下载一个免费的U盘制作工具rufus
    此时插入已经格式化的U盘,打开rufus,一般情况下它会自动选择U盘,你也可以在device选项下手动选择或确认。
    点击select,选择之前下载好的镜像文件。
    其他设置保留默认即可,不放心的话可以比对下图: 然后start开始制作。如果此时rufus提示需要下载一些其它文件,选择Yes继续即可。
    没有问题的话制作完的U盘会如图所示: 在下面的步骤中,请一直插着U盘不要拔。

安装

现在重新启动电脑,开机的过程中不停地快按F12进入bios界面(我的是戴尔的电脑,不同电脑按键或有不同,自行百度;如果快按不行的话再次重启试一试长按,因为网上有些教程说的是长按,而我是长按不行而快按可以)。
随后选择U盘启动(不同电脑这个界面也可能不一样,具体可以百度,我选择的是UEFI BOOT中UEFI:U盘名那项)。
接下来就进入了紫红色的GNU GRUB界面,选择install ubuntu。如果在这里迟疑了一下,会自动进入trying without install,这也没关系,你也可以进入后在图形界面中双击安装,安装之后可以继续试用,直到重启。
随后就是些比较简单的安装过程,基本上可以按默认进行,因为是重装,好像不需要联网安装且有汉化包。

注意:若是安装18.04LTS,这里会出现一个“正常安装”还是“最小安装”的选择,一般无脑选择正常安装即可,但请事先对安装时间做好心里准备(我大概花了一个多小时)且安装完后有些软件还是比较多余的,可以手动卸载。

接下来是比较重要的部分:
进入安装类型installation type界面后,选择其他选项something else,这样我们就可以自己配置ubuntu分区了,点击继续。
接下来会进入一个磁盘分区的界面,选中之前清出来的未分配分区(名为“空闲”,也可以通过大小来判断),点击下方+号,新建一个swap交换分区,大小为8G左右(一般和电脑的内存相当即可,不分这个区会有警告,也可不分之后再加)。
再次双击空闲分区,挂载点下拉,选择/(相当于windows的C盘)。
在安装启动引导器的设备选项中,选择Windows Boot Manager。
结果可以参考下图:

确认无误后点击现在安装,然后就一路默认直到安装完成。
这里会有一个设置用户名和计算机名的界面,建议设置得短一些比较好,否则在终端中每条键入的命令前都会有很长的一串“用户名@计算机名”。
安装完成后可以进行试用,此时一切操作都不会被保留。
如果无需试用,就重新启动系统,此时会提醒可以拔出installation medium即启动U盘。


后期

别忘了把U盘格式化回来,可以继续使用,留着做纪念也行,说不定哪天又要重装。
下面我展示一下我目前的一部分美化效果,亲测发现这只会牺牲一点点儿CPU,所以并不用担心,大胆地美化就是,可能这也是使用linux发行版不多的几种乐趣之一吧。





以上就是重装ubuntu的全部内容,欢迎补充!我也会在新问题出现时及时更新。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:重装ubuntu——记一段辛酸血泪史

文章作者:高深远

发布时间:2019年09月14日 - 10:00

最后更新:2020年02月07日 - 16:58

原始链接:https://gsy00517.github.io/ubuntu20190914100050/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:双系统下时间差问题的解决 | 高深远的博客

ubuntu笔记:双系统下时间差问题的解决

第一次在电脑加装ubuntu双系统后,就存在ubuntu比windows系统时间慢8个小时的问题。当时搞了一会好像也解决了。然而,在我重装了ubuntu系统之后(详见ubuntu笔记:重装ubuntu——记一段辛酸血泪史),这个问题又出现了。一时间得不到很好地解决,也就没管。最近强迫症犯了,花了点功夫终于搞定了,决定记录在此。

References

电子文献:
http://doc.ntp.org/4.1.1/ntpdate.htm
http://doc.ntp.org/4.1.1/ntpd.htm
https://blog.csdn.net/vic_qxz/article/details/80344855


windows、ubuntu系统时间

在windows中,系统时间的设置较为简单。而且设置后,系统时间会自动保存在bios的时钟里面,当启动计算机时,系统会自动在bios里面读取硬件时间,以保证时间不间断。
但在ubuntu linux默认情况下,系统时间和硬件时间,并不会自动同步。在ubuntu linux运行过程中,系统时间和硬件时间以异步的方式运行,互不干扰。硬件时间是靠bios电池来维持运行的,而系统时间是用CPU tick来维持的。在系统开机时,会自动从bios中取得硬件时间,设置为系统时间。
这样一来就不奇怪了,中国的时区是东八区(GMT+8),因此ubuntu每次读入的是格林威治标准时间并直接将其设置为系统时间,而windows则会加上8:00调整。
因此我解决的思路如下:考虑到windows下时间调整更方便,我就优先调整ubuntu的系统时间,将其系统时间(即本电脑的硬件时间)设为GMT+8,然后再在windows系统中取消自动添加8小时的自动调整,即让两个系统以同样的方法从硬件时间设置系统时间。


ntpd

这里先介绍一下ntpd(Network Time Protocol (NTP) daemon),如官方文档所说,它的作用是sets and maintains the system time of day in synchronism with Internet standard time servers。因此,我们可以通过ntpdate命令进行设置,其基本格式如下:

1
ntpdate [ -bBdoqsuv ] [ -a key ] [ -e authdelay ] [ -k keyfile ] [ -o version ] [ -p samples ] [ -t timeout ] server [ ... ]

这里我们不需要用到上面的这些额外选项,因此不一一介绍了。


solution

  1. 安装

    如果还没有安装ntpdate的话,可以先执行该条命令。

    1
    sudo apt-get install ntpdate

    注意:apt-get大部分操作都需要root权限,别忘了sudo赋予权限。我有一回忘记sudo了结果搞了半天不知所以…真的太蠢了。

  2. 从服务器校准时间

    这里我使用的时间服务器是time.windows.com,好像也可以用苹果的time.apple.com或者阿里云的time.pool.aliyun.com

    1
    sudo ntpdate time.windows.com

    格式参考上文。在执行之后发现有0.005秒的微小偏差,因此感觉还是比较可靠的。

  3. 把时间同步到硬件上

    同步系统时间和硬件时间,可以使用hwclock命令。

    1
    sudo hwclock --localtime --systohc

    这里的sysyohc即系统时间(sys)写到(to)硬件时间(hard clock)。
    这时ubuntu这边已经解决了,但如果重启打开windows,会发现时间快了8小时,原因之前解释过,因为自动加了8小时,所以还要作下面的调整。

  4. 调整windows

    打开windows,调整日期/时间,把时区改到:(UTC)协调世界时。如此一来,windows上的系统时间也是硬件时间了。双系统的系统时间设置方式一致,时间准确,大功告成。

    注:如果windows中时间没有问题,那就无需调整时区。总之就是先设置好ubuntu的,然后再在windows里调整,因为windows下更好调整。似乎windows会自动更正系统时间,所以经以上4步操作后过段时间需要把时区调整回来。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:双系统下时间差问题的解决

文章作者:高深远

发布时间:2020年01月17日 - 08:53

最后更新:2020年01月19日 - 08:26

原始链接:https://gsy00517.github.io/ubuntu20200117085337/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:双系统下时间差问题的解决 | 高深远的博客

ubuntu笔记:双系统下时间差问题的解决

第一次在电脑加装ubuntu双系统后,就存在ubuntu比windows系统时间慢8个小时的问题。当时搞了一会好像也解决了。然而,在我重装了ubuntu系统之后(详见ubuntu笔记:重装ubuntu——记一段辛酸血泪史),这个问题又出现了。一时间得不到很好地解决,也就没管。最近强迫症犯了,花了点功夫终于搞定了,决定记录在此。

References

电子文献:
http://doc.ntp.org/4.1.1/ntpdate.htm
http://doc.ntp.org/4.1.1/ntpd.htm
https://blog.csdn.net/vic_qxz/article/details/80344855


windows、ubuntu系统时间

在windows中,系统时间的设置较为简单。而且设置后,系统时间会自动保存在bios的时钟里面,当启动计算机时,系统会自动在bios里面读取硬件时间,以保证时间不间断。
但在ubuntu linux默认情况下,系统时间和硬件时间,并不会自动同步。在ubuntu linux运行过程中,系统时间和硬件时间以异步的方式运行,互不干扰。硬件时间是靠bios电池来维持运行的,而系统时间是用CPU tick来维持的。在系统开机时,会自动从bios中取得硬件时间,设置为系统时间。
这样一来就不奇怪了,中国的时区是东八区(GMT+8),因此ubuntu每次读入的是格林威治标准时间并直接将其设置为系统时间,而windows则会加上8:00调整。
因此我解决的思路如下:考虑到windows下时间调整更方便,我就优先调整ubuntu的系统时间,将其系统时间(即本电脑的硬件时间)设为GMT+8,然后再在windows系统中取消自动添加8小时的自动调整,即让两个系统以同样的方法从硬件时间设置系统时间。


ntpd

这里先介绍一下ntpd(Network Time Protocol (NTP) daemon),如官方文档所说,它的作用是sets and maintains the system time of day in synchronism with Internet standard time servers。因此,我们可以通过ntpdate命令进行设置,其基本格式如下:

1
ntpdate [ -bBdoqsuv ] [ -a key ] [ -e authdelay ] [ -k keyfile ] [ -o version ] [ -p samples ] [ -t timeout ] server [ ... ]

这里我们不需要用到上面的这些额外选项,因此不一一介绍了。


solution

  1. 安装

    如果还没有安装ntpdate的话,可以先执行该条命令。

    1
    sudo apt-get install ntpdate

    注意:apt-get大部分操作都需要root权限,别忘了sudo赋予权限。我有一回忘记sudo了结果搞了半天不知所以…真的太蠢了。

  2. 从服务器校准时间

    这里我使用的时间服务器是time.windows.com,好像也可以用苹果的time.apple.com或者阿里云的time.pool.aliyun.com

    1
    sudo ntpdate time.windows.com

    格式参考上文。在执行之后发现有0.005秒的微小偏差,因此感觉还是比较可靠的。

  3. 把时间同步到硬件上

    同步系统时间和硬件时间,可以使用hwclock命令。

    1
    sudo hwclock --localtime --systohc

    这里的sysyohc即系统时间(sys)写到(to)硬件时间(hard clock)。
    这时ubuntu这边已经解决了,但如果重启打开windows,会发现时间快了8小时,原因之前解释过,因为自动加了8小时,所以还要作下面的调整。

  4. 调整windows

    打开windows,调整日期/时间,把时区改到:(UTC)协调世界时。如此一来,windows上的系统时间也是硬件时间了。双系统的系统时间设置方式一致,时间准确,大功告成。

    注:如果windows中时间没有问题,那就无需调整时区。总之就是先设置好ubuntu的,然后再在windows里调整,因为windows下更好调整。似乎windows会自动更正系统时间,所以经以上4步操作后过段时间需要把时区调整回来。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:双系统下时间差问题的解决

文章作者:高深远

发布时间:2020年01月17日 - 08:53

最后更新:2020年01月19日 - 08:26

原始链接:https://gsy00517.github.io/ubuntu20200117085337/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:apt包管理以及如何更新软件列表 | 高深远的博客

ubuntu笔记:apt包管理以及如何更新软件列表

ubuntu有时在用户登录后会提示有软件包更新,每次更新之后按提示重启,你就会看到一个类似于安全模式下大写的GNU GRUB(一个多操作系统启动程序),虽然这没什么问题,但是我在想能不能自主地去更新呢?

References

电子文献:
https://birdteam.net/122231
https://blog.csdn.net/a3192048/article/details/86618314


apt-get

这个有点类似于windows中的dism命令,可以用于安装、更新、卸载软件,大部分操作需要root权限,因此使用命令时别忘了授权。
首先介绍一下它的常见用法:

  1. 安装

    使用如下命令安装名为xxx的软件:

    1
    sudo apt-get install xxx
  2. 卸载

    使用如下命令卸载名为xxx的软件:

    1
    sudo apt-get remove xxx

    注意:切忌卸载关键的软件包,比如coreutils。

  3. 更新

    本文重点来了,apt-get相关升级更新命令有下面这四个:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    sudo apt-get update
    #更新软件源缓存,从服务器更新可用的软件列表,一般在安装软件时引入新的软件仓库之后使用

    sudo apt-get upgrade
    #更新系统,即根据列表更新已安装的软件包,既不会删除在列表中已经不存在了的软件,也不会安装有依赖需求但尚未安装的软件

    sudo apt-get full-upgrade
    #根据列表更新已安装的软件包,可能会为了解决软件包冲突而删除一些已安装的软件

    sudo apt-get dist-upgrade
    #更新系统版本,也是根据列表更新已安装的软件包,可能会为了解决软件包冲突而删除一些已安装的软件,不同于full-upgrade的dist-upgrade也可能会为了解决软件包依赖问题安装新的软件包

更新软件列表

当我们想自主更新软件包时,可以依次执行下面两条命令:

1
2
sudo apt-get upgrade
sudo apt-get dist-upgrade //谨慎执行

这两条命令其实比较类似,不同的是当相依性问题时,upgrade时此package就不会被升级而保留下来;而dist-upgrade相对“智能”,若遇到相依性问题,需要安装或者移除新的package时,dist-upgrade命令就会试着去安装或者移除它,这就可能以牺牲某些非重要软件包为代价来升级某些非常重要的软件包,个人认为存在一定风险。


apt

在根据各类教程安装各个软件时,我开始注意到有时候apt-get的位置被apt代替了。随着使用量的增加,这个疑惑越来越大,因此我决定搞搞清楚。
其实,apt命令是在ubuntu16.04发布时引入的。它具有更精减但足够的命令选项,而且具有更为有效的参数选项的组织方式。实际上,虽然不是一个东西,但完完全全可以认为aptapt-get是等价的,其格式语法几乎完全统一,在使用时不会出现不同。目前apt命令还在不断地发展,而apt-getapt有更多、更细化的操作功能,有时对于一些低级操作,仍需使用apt-get
下表是apt命令与apt-get等命令的对比,可以看到在普通使用时是完全一样的。

apt命令等效命令功能
apt installapt-get install安装软件包
apt removeapt-get remove移除软件包
apt purgeapt-get purge移除软件包及配置文件
apt updateapt-get update更新软件列表
apt upgradeapt-get upgrade升级所有可升级的软件包
apt autoremoveapt-get autoremove自动删除不需要的包
apt full-upgradeapt-get full-upgrade在升级软件包时自动处理依赖关系
apt searchapt-get search搜索应用程序
apt showapt-get show显示软件包信息

此外,apt还有一些自己的命令,比如apt list列出包含条件的包(已安装,可升级等);apt edit-sources编辑源列表。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:apt包管理以及如何更新软件列表

文章作者:高深远

发布时间:2020年01月17日 - 09:44

最后更新:2020年01月27日 - 16:24

原始链接:https://gsy00517.github.io/ubuntu20200117094401/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:apt包管理以及如何更新软件列表 | 高深远的博客

ubuntu笔记:apt包管理以及如何更新软件列表

ubuntu有时在用户登录后会提示有软件包更新,每次更新之后按提示重启,你就会看到一个类似于安全模式下大写的GNU GRUB(一个多操作系统启动程序),虽然这没什么问题,但是我在想能不能自主地去更新呢?

References

电子文献:
https://birdteam.net/122231
https://blog.csdn.net/a3192048/article/details/86618314


apt-get

这个有点类似于windows中的dism命令,可以用于安装、更新、卸载软件,大部分操作需要root权限,因此使用命令时别忘了授权。
首先介绍一下它的常见用法:

  1. 安装

    使用如下命令安装名为xxx的软件:

    1
    sudo apt-get install xxx
  2. 卸载

    使用如下命令卸载名为xxx的软件:

    1
    sudo apt-get remove xxx

    注意:切忌卸载关键的软件包,比如coreutils。

  3. 更新

    本文重点来了,apt-get相关升级更新命令有下面这四个:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    sudo apt-get update
    #更新软件源缓存,从服务器更新可用的软件列表,一般在安装软件时引入新的软件仓库之后使用

    sudo apt-get upgrade
    #更新系统,即根据列表更新已安装的软件包,既不会删除在列表中已经不存在了的软件,也不会安装有依赖需求但尚未安装的软件

    sudo apt-get full-upgrade
    #根据列表更新已安装的软件包,可能会为了解决软件包冲突而删除一些已安装的软件

    sudo apt-get dist-upgrade
    #更新系统版本,也是根据列表更新已安装的软件包,可能会为了解决软件包冲突而删除一些已安装的软件,不同于full-upgrade的dist-upgrade也可能会为了解决软件包依赖问题安装新的软件包

更新软件列表

当我们想自主更新软件包时,可以依次执行下面两条命令:

1
2
sudo apt-get upgrade
sudo apt-get dist-upgrade //谨慎执行

这两条命令其实比较类似,不同的是当相依性问题时,upgrade时此package就不会被升级而保留下来;而dist-upgrade相对“智能”,若遇到相依性问题,需要安装或者移除新的package时,dist-upgrade命令就会试着去安装或者移除它,这就可能以牺牲某些非重要软件包为代价来升级某些非常重要的软件包,个人认为存在一定风险。


apt

在根据各类教程安装各个软件时,我开始注意到有时候apt-get的位置被apt代替了。随着使用量的增加,这个疑惑越来越大,因此我决定搞搞清楚。
其实,apt命令是在ubuntu16.04发布时引入的。它具有更精减但足够的命令选项,而且具有更为有效的参数选项的组织方式。实际上,虽然不是一个东西,但完完全全可以认为aptapt-get是等价的,其格式语法几乎完全统一,在使用时不会出现不同。目前apt命令还在不断地发展,而apt-getapt有更多、更细化的操作功能,有时对于一些低级操作,仍需使用apt-get
下表是apt命令与apt-get等命令的对比,可以看到在普通使用时是完全一样的。

apt命令等效命令功能
apt installapt-get install安装软件包
apt removeapt-get remove移除软件包
apt purgeapt-get purge移除软件包及配置文件
apt updateapt-get update更新软件列表
apt upgradeapt-get upgrade升级所有可升级的软件包
apt autoremoveapt-get autoremove自动删除不需要的包
apt full-upgradeapt-get full-upgrade在升级软件包时自动处理依赖关系
apt searchapt-get search搜索应用程序
apt showapt-get show显示软件包信息

此外,apt还有一些自己的命令,比如apt list列出包含条件的包(已安装,可升级等);apt edit-sources编辑源列表。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:apt包管理以及如何更新软件列表

文章作者:高深远

发布时间:2020年01月17日 - 09:44

最后更新:2020年01月27日 - 16:24

原始链接:https://gsy00517.github.io/ubuntu20200117094401/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:删除文件中的问题 | 高深远的博客

ubuntu笔记:删除文件中的问题

大家可以看看我删个文件多么曲折:

献丑了哈哈哈,这里就对这个过程中涉及到的一些问题做一个总结吧。


目录

首先我cd /进入到根目录,然后我每一步ls列出目录中的文件及子目录,一步一个脚印找到了我要删的文件——MATLAB,emmm我不想解释为什么是它。
然后我想当然的想remove掉这个文件,结果发现权限不够。这里其实可以ls -l以列表的形式查看目录中的文件及子目录并且列出每个文件拥有者、所属组、其他用户各自的权限的。
后面我又使用cd ../来回到上一级目录,这是为了怕自己搞错目录,怕删高了一级酿成惨剧。


权限

它说我没权限,于是就sudo临时给个5分钟的root权限呗。本来还想sudo su进入root的(可以用ctrl+D退出),那简直杀鸡用牛刀了。


删除文件/目录

一开始用rm,它提示我是一个目录,于是我使用了rmdir,但它的作用是删除一个空目录,而我的目录内还有文件。
于是我使用sudo rm folder_name -R即递归删除文件的方法来从里到外把这个目录中的文件都删了。
其实好像也可以sudo rm -rf folder_name强制删除,这里-r-R一样,都是递归的意思,-f就是强制执行无需确认。但是由于牢记linux最大禁忌rm -rf /*(真正的从删库到跑路),对这个命令还是比较怕的,于是就采取了前者。执行完之后再ls看了一下,发现已成功删除,df查看空间分配,内存使用也回来了不少。

补充:-R递归也有许多别的妙用,比如可以通过sudo chmod a+rw file_name -R来一次性修改一个文件夹内所有文件的权限。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:删除文件中的问题

文章作者:高深远

发布时间:2020年01月17日 - 21:39

最后更新:2020年01月24日 - 18:32

原始链接:https://gsy00517.github.io/ubuntu20200117213946/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:删除文件中的问题 | 高深远的博客

ubuntu笔记:删除文件中的问题

大家可以看看我删个文件多么曲折:

献丑了哈哈哈,这里就对这个过程中涉及到的一些问题做一个总结吧。


目录

首先我cd /进入到根目录,然后我每一步ls列出目录中的文件及子目录,一步一个脚印找到了我要删的文件——MATLAB,emmm我不想解释为什么是它。
然后我想当然的想remove掉这个文件,结果发现权限不够。这里其实可以ls -l以列表的形式查看目录中的文件及子目录并且列出每个文件拥有者、所属组、其他用户各自的权限的。
后面我又使用cd ../来回到上一级目录,这是为了怕自己搞错目录,怕删高了一级酿成惨剧。


权限

它说我没权限,于是就sudo临时给个5分钟的root权限呗。本来还想sudo su进入root的(可以用ctrl+D退出),那简直杀鸡用牛刀了。


删除文件/目录

一开始用rm,它提示我是一个目录,于是我使用了rmdir,但它的作用是删除一个空目录,而我的目录内还有文件。
于是我使用sudo rm folder_name -R即递归删除文件的方法来从里到外把这个目录中的文件都删了。
其实好像也可以sudo rm -rf folder_name强制删除,这里-r-R一样,都是递归的意思,-f就是强制执行无需确认。但是由于牢记linux最大禁忌rm -rf /*(真正的从删库到跑路),对这个命令还是比较怕的,于是就采取了前者。执行完之后再ls看了一下,发现已成功删除,df查看空间分配,内存使用也回来了不少。

补充:-R递归也有许多别的妙用,比如可以通过sudo chmod a+rw file_name -R来一次性修改一个文件夹内所有文件的权限。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:删除文件中的问题

文章作者:高深远

发布时间:2020年01月17日 - 21:39

最后更新:2020年01月24日 - 18:32

原始链接:https://gsy00517.github.io/ubuntu20200117213946/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:安装typora | 高深远的博客

ubuntu笔记:安装typora

今天ubuntu系统又双叒叕被我搞崩了,折腾一个大半天之后还是无解,没办法只好根据之前博文ubuntu笔记:重装ubuntu——记一段辛酸血泪史中的方法重装系统。心里还是非常庆幸还好当初留心写了一下。
痛定思痛,由于之前没有系统地学习linux操作系统,鸟哥的书也就看了一部分,因此觉得自己以后应该更加谨慎小心一些,每一步命令都要看明白再执行,不然再翻车的话真的要心态爆炸的。
之前在markdown笔记:markdown的基本使用中介绍过typora,这里主要是以它为例,仔细地分析一下安装软件时每一步命令的作用。

References

电子文献:
https://support.typora.io/Typora-on-Linux/


安装过程

  1. 信任软件包密匙

    1
    2
    sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA300B7755AFCFAE
    #optional, but recommended

    这条命令应该就是添加新的密匙并信任,一般在配置apt-get源之前运行。
    apt-key的描述如下:“apt-key is used to manage the list of keys used by apt to authenticate packages. Packages which have been authenticated using these keys will be considered trusted.”由于每个发布的Debian软件包都是通过密钥认证的,而apt-key命令正是用来管理Debian软件包密钥的。

  2. 添加软件库

    由于默认的软件仓库里是没有typora的,所以要添加对应的软件仓库。

    1
    2
    sudo add-apt-repository 'deb https://typora.io/linux ./'
    #add Typora's repository
  3. 更新软件列表

    在添加了新的软件仓库之后,我们需要更新软件列表使得后面的操作能找到对应的软件包。

    1
    sudo apt-get update
  4. 安装

    更新apt-get之后,就可以安装前面添加的库中的软件包了。

    1
    2
    sudo apt-get install typora
    #install typora
  5. 有软件包无法下载

    在install的过程中,提示我:“有几个软件包无法下载”。
    于是我照着提示执行了下面的命令:

    1
    sudo apt-get update --fix-missing

    然后再sudo apt-get install typora,就可以了。
    如果还是有问题的话,可能需要更换软件源,换成国内的镜像比较好。

  6. 更新

    安装后的typora由apt-get管理,因此可以用以下命令来更新软件包。
    1
    sudo apt-get upgrade

软连接

痛定思痛,还是决定把这回翻车的地方写一下。
本来用命令行打开matlab挺好的,我自作自受想转个matlab-support想着用图标打开,结果报错:MATLAB is selecting SOFTWARE OPENGL rendering。到网上查资料后找到一个貌似可行的方法。
根据他所说,这是因为matlab的libstdc++库和系统库不匹配造成的,所以需要用如下命令建立一个连接。

1
ln -s /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21  /usr/local/MATLAB/R2015b/sys/os/glnxa64/libstdc++.so.6

注意,这里是R2015b。由于我下载的是R2018b,显然这里的地址是不一样的,当时比较心急直接回车了。结果还是没有解决问题。
这句命令其实就是建立一个软连接,其基本格式是ln –s 源文件目录 目标文件目录。它只会在选定的位置上生成一个文件的镜像,不会占用磁盘空间,类似于windows中的快捷方式。若没加-s,就是硬链接,即会在选定的位置上生成一个和源文件大小相同的文件。不过,无论是软链接还是硬链接,文件都保持同步变化。

讲道理即使目录出错也是不会有问题的,然而当我再次开机尝试进入系统时,就出现了卡在recovering journal的情况。
卡住的位置仅有两行,第一行是recovering journal,第二行我在ubuntu社区里找到了一个比较类似的,如下图所示。

他后面解答的方法如下。

可以试一试,我也照着做下来了,但是没起作用。
我也在网上看了其它的一些办法,有先进入recovery mode然后选择resume normal boot就好了的(就是返回正常启动,很玄学),然而我没用;也有check all file systems的,我也尝试了but failed。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:安装typora

文章作者:高深远

发布时间:2020年01月23日 - 10:39

最后更新:2020年01月30日 - 19:53

原始链接:https://gsy00517.github.io/ubuntu20200123103954/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:安装typora | 高深远的博客

ubuntu笔记:安装typora

今天ubuntu系统又双叒叕被我搞崩了,折腾一个大半天之后还是无解,没办法只好根据之前博文ubuntu笔记:重装ubuntu——记一段辛酸血泪史中的方法重装系统。心里还是非常庆幸还好当初留心写了一下。
痛定思痛,由于之前没有系统地学习linux操作系统,鸟哥的书也就看了一部分,因此觉得自己以后应该更加谨慎小心一些,每一步命令都要看明白再执行,不然再翻车的话真的要心态爆炸的。
之前在markdown笔记:markdown的基本使用中介绍过typora,这里主要是以它为例,仔细地分析一下安装软件时每一步命令的作用。

References

电子文献:
https://support.typora.io/Typora-on-Linux/


安装过程

  1. 信任软件包密匙

    1
    2
    sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA300B7755AFCFAE
    #optional, but recommended

    这条命令应该就是添加新的密匙并信任,一般在配置apt-get源之前运行。
    apt-key的描述如下:“apt-key is used to manage the list of keys used by apt to authenticate packages. Packages which have been authenticated using these keys will be considered trusted.”由于每个发布的Debian软件包都是通过密钥认证的,而apt-key命令正是用来管理Debian软件包密钥的。

  2. 添加软件库

    由于默认的软件仓库里是没有typora的,所以要添加对应的软件仓库。

    1
    2
    sudo add-apt-repository 'deb https://typora.io/linux ./'
    #add Typora's repository
  3. 更新软件列表

    在添加了新的软件仓库之后,我们需要更新软件列表使得后面的操作能找到对应的软件包。

    1
    sudo apt-get update
  4. 安装

    更新apt-get之后,就可以安装前面添加的库中的软件包了。

    1
    2
    sudo apt-get install typora
    #install typora
  5. 有软件包无法下载

    在install的过程中,提示我:“有几个软件包无法下载”。
    于是我照着提示执行了下面的命令:

    1
    sudo apt-get update --fix-missing

    然后再sudo apt-get install typora,就可以了。
    如果还是有问题的话,可能需要更换软件源,换成国内的镜像比较好。

  6. 更新

    安装后的typora由apt-get管理,因此可以用以下命令来更新软件包。
    1
    sudo apt-get upgrade

软连接

痛定思痛,还是决定把这回翻车的地方写一下。
本来用命令行打开matlab挺好的,我自作自受想转个matlab-support想着用图标打开,结果报错:MATLAB is selecting SOFTWARE OPENGL rendering。到网上查资料后找到一个貌似可行的方法。
根据他所说,这是因为matlab的libstdc++库和系统库不匹配造成的,所以需要用如下命令建立一个连接。

1
ln -s /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21  /usr/local/MATLAB/R2015b/sys/os/glnxa64/libstdc++.so.6

注意,这里是R2015b。由于我下载的是R2018b,显然这里的地址是不一样的,当时比较心急直接回车了。结果还是没有解决问题。
这句命令其实就是建立一个软连接,其基本格式是ln –s 源文件目录 目标文件目录。它只会在选定的位置上生成一个文件的镜像,不会占用磁盘空间,类似于windows中的快捷方式。若没加-s,就是硬链接,即会在选定的位置上生成一个和源文件大小相同的文件。不过,无论是软链接还是硬链接,文件都保持同步变化。

讲道理即使目录出错也是不会有问题的,然而当我再次开机尝试进入系统时,就出现了卡在recovering journal的情况。
卡住的位置仅有两行,第一行是recovering journal,第二行我在ubuntu社区里找到了一个比较类似的,如下图所示。

他后面解答的方法如下。

可以试一试,我也照着做下来了,但是没起作用。
我也在网上看了其它的一些办法,有先进入recovery mode然后选择resume normal boot就好了的(就是返回正常启动,很玄学),然而我没用;也有check all file systems的,我也尝试了but failed。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:安装typora

文章作者:高深远

发布时间:2020年01月23日 - 10:39

最后更新:2020年01月30日 - 19:53

原始链接:https://gsy00517.github.io/ubuntu20200123103954/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:安装与卸载deb软件包 | 高深远的博客

ubuntu笔记:安装与卸载deb软件包

似乎是为了支持由武汉深之度科技开发的国产linux系统Deepin,近年来许多常用软件都提供了linux客户端,比如QQ for linux,baidunetdisk for linux。然而我安装百度网盘后发现打不开,一打开就报错,后来才知道百度网盘仅支持ubuntu18之后的版本。于是就又涉及到deb包的卸载问题了。

References

电子文献:
https://askubuntu.com/questions/18804/what-do-the-various-dpkg-flags-like-ii-rc-mean
https://blog.csdn.net/sun2333/article/details/82707362


dpkg flag

我们可以使用dpkg -l | grep '软件名'来查看相应软件的安装状态,这时一般会出现有两个字母组成的一个flag。具体可以看后文中的截图。这里我想先整理一下这两个字母的含义。

  1. 第一个字母:所需的状态desired package state(”selection state”)

    • u——未知unknown
    • i——安装install
    • r——删除/卸载remove/deinstall
    • p——清除(除包含配置文件)purge(remove including config files)
    • h——保持hold
  2. 第二个字母:当前包状态current package state

    • n——未安装not-installed
    • i——已安装installed
    • c——仅安装配置文件config-files(only the config files are installed)
    • U——解包unpacked
    • F——由于某种原因配置失败half-configured(configuration failed for some reason)
    • h——由于某种原因安装失败half-installed(installation failed for some reason)
    • W——包正在等待来自另一个包的触发器triggers-awaited(package is waiting for a trigger from another package)
    • t——包已被触发triggers-pending(package has been triggered)
  3. 第三个字母:错误状态error state

    第三个字母通常情况下是一个空格,一般不会看到。
    • R——包破损,需要重新安装reinst-required(package broken, reinstallation required)

安装

使用如下命令进行安装。

1
sudo dpkg -i package-file-name

这里的-i表示的是install。注意,这里的package-file-name包括后缀如“.deb”。


卸载

下面这张图就是我卸载的过程。

首先我使用了dpkg -l | grep '软件名'命令来查看我系统上百度网盘的安装状态。结果显示为“ii”,表示“installed ok installed”即它应该被安装并且已安装。
随后,利用-r参数,使用下面命令进行移除。

1
sudo dpkg -r 软件名

注意,这里的软件名不需要添加引号。
移除之后,我们可以再次使用dpkg -l | grep '软件名'来查看百度网盘的安装状态。结果显示为“rc”,表示“removed ok config-files”即它已经被移除/卸载,但它的配置文件仍然存在。
这时我们也是使用如下命令来彻底卸载软件包(包括配置文件)。

1
sudo dpkg -P 软件名

ubuntu笔记:释放空间一文中,有一次性清理所有残留配置文件的方法,可以看一下。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:安装与卸载deb软件包

文章作者:高深远

发布时间:2020年01月26日 - 08:34

最后更新:2020年01月26日 - 10:12

原始链接:https://gsy00517.github.io/ubuntu20200126083448/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:安装与卸载deb软件包 | 高深远的博客

ubuntu笔记:安装与卸载deb软件包

似乎是为了支持由武汉深之度科技开发的国产linux系统Deepin,近年来许多常用软件都提供了linux客户端,比如QQ for linux,baidunetdisk for linux。然而我安装百度网盘后发现打不开,一打开就报错,后来才知道百度网盘仅支持ubuntu18之后的版本。于是就又涉及到deb包的卸载问题了。

References

电子文献:
https://askubuntu.com/questions/18804/what-do-the-various-dpkg-flags-like-ii-rc-mean
https://blog.csdn.net/sun2333/article/details/82707362


dpkg flag

我们可以使用dpkg -l | grep '软件名'来查看相应软件的安装状态,这时一般会出现有两个字母组成的一个flag。具体可以看后文中的截图。这里我想先整理一下这两个字母的含义。

  1. 第一个字母:所需的状态desired package state(”selection state”)

    • u——未知unknown
    • i——安装install
    • r——删除/卸载remove/deinstall
    • p——清除(除包含配置文件)purge(remove including config files)
    • h——保持hold
  2. 第二个字母:当前包状态current package state

    • n——未安装not-installed
    • i——已安装installed
    • c——仅安装配置文件config-files(only the config files are installed)
    • U——解包unpacked
    • F——由于某种原因配置失败half-configured(configuration failed for some reason)
    • h——由于某种原因安装失败half-installed(installation failed for some reason)
    • W——包正在等待来自另一个包的触发器triggers-awaited(package is waiting for a trigger from another package)
    • t——包已被触发triggers-pending(package has been triggered)
  3. 第三个字母:错误状态error state

    第三个字母通常情况下是一个空格,一般不会看到。
    • R——包破损,需要重新安装reinst-required(package broken, reinstallation required)

安装

使用如下命令进行安装。

1
sudo dpkg -i package-file-name

这里的-i表示的是install。注意,这里的package-file-name包括后缀如“.deb”。


卸载

下面这张图就是我卸载的过程。

首先我使用了dpkg -l | grep '软件名'命令来查看我系统上百度网盘的安装状态。结果显示为“ii”,表示“installed ok installed”即它应该被安装并且已安装。
随后,利用-r参数,使用下面命令进行移除。

1
sudo dpkg -r 软件名

注意,这里的软件名不需要添加引号。
移除之后,我们可以再次使用dpkg -l | grep '软件名'来查看百度网盘的安装状态。结果显示为“rc”,表示“removed ok config-files”即它已经被移除/卸载,但它的配置文件仍然存在。
这时我们也是使用如下命令来彻底卸载软件包(包括配置文件)。

1
sudo dpkg -P 软件名

ubuntu笔记:释放空间一文中,有一次性清理所有残留配置文件的方法,可以看一下。


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:安装与卸载deb软件包

文章作者:高深远

发布时间:2020年01月26日 - 08:34

最后更新:2020年01月26日 - 10:12

原始链接:https://gsy00517.github.io/ubuntu20200126083448/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:snap软件包管理及问题 | 高深远的博客

ubuntu笔记:snap软件包管理及问题

在我安装ubuntu18.04LTS的时候,由于下载语言包是真的久,我就翻了一下ubuntu安装界面的介绍,其中一开始就是对snap store的介绍。

这篇文章就说说snap和我之前遇到的问题。

References

电子文献:
https://www.jb51.net/article/128368.htm
https://blog.csdn.net/u011870280/article/details/80213866


snap

snap是ubuntu母公司Canonical于2016年4月发布ubuntu16.04时候引入的一种安全的、易于管理的、沙盒化的软件包格式,与传统的dpkg和apt有着很大的区别。在ubuntu软件中心下载安装的似乎都是snap管理的。这让一些商业闭源软件也能在linux上发布,说白了是ubuntu为了获得linux发行版霸权的一个重要举措,因此没少招黑。知乎上看到这么一句话,笑半天:“Fuck the political correct, make linux great again.(says 川·乌班图·普)”。


常用命令

  1. 列出已经安装的snap包

    1
    sudo snap list
  2. 搜索要安装的snap包

    1
    sudo snap find <text to search>
  3. 安装一个snap包

    1
    sudo snap install <snap name>
  4. 更新一个snap包

    1
    sudo snap refresh <snap name>

    注:如果后面不加包的名字就更新所有的snap包。

  5. 把一个包还原到以前安装的版本

    1
    sudo snap revert <snap name>
  6. 删除一个snap包

    1
    sudo snap remove <snap name>
  7. 查看最近的更改

    1
    snap changes
  8. 终止snap进程

    1
    sudo snap abort <进程序号>

后面两个命令将在下面的问题中发挥作用。


问题

当我在snap store也就是ubuntu软件中心下载pycharm和VScode时,遇到了如下报错:

1
snapd returned status code 409: Conflict

上网查找之后,才知道这个错误码409表示的是:由于和被请求的资源的当前状态之间存在冲突,请求无法完成。即并发执行时返回的错误码。
由于之前ubuntu软件中心无响应被我强制退出了,因此的确很有可能与之前进行到一半的安装冲突。于是使用snap changes查看最近的snap更改。

果然看见之前的snap进程依旧在“Doing”,因此根据对应的序号使用sudo snap abort终止进程。
这时再回到软件中心安装,就没有之前的报错了。
然而…


关于国内使用snap

因为网络原因,而且也没有可用的镜像,导致snapcraft在中国大陆地区访问速度非常非常慢,下载软件需要很长的时间并且很容易中途出错。
此外,由于snap软件会把主分区分成好多个loop,看起来真的不想说什么了。图源自贴吧,可以看到这挂载的snap软件包可以说是相当壮(别)观(扭)了。

还有一个杀死强迫症(比如我)的问题就是,snap会在家目录(即18.04的主文件夹)中创建一个snap文件夹,里面各种快捷方式、循环嵌套的文件夹,害…无法用语言描述,看了就知道,总之就是非常不爽。主要是一些资料、文件一般也会放在家目录下面,看到了snap在那边亮眼睛真的难受。
实在不知道为什么社区里有不少人推崇snap(不过国外没速度限制,snap对他们来说挺方便的)。
总而言之,综合速度(硬伤)和美观舒适度考虑,还是尽量避免使用snap命令安装软件,也不要下载ubuntu软件商店中的snap格式的软件包(基本都是)。甚至有些“安装ubuntu之后必做的…件事”等诸如此类的ubuntu配置或者美化的教程内直接把卸载snap列作其中一项哈哈。
总之管理软件还是apt优先,详见我的博文ubuntu笔记:apt包管理以及如何更新软件列表


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:snap软件包管理及问题

文章作者:高深远

发布时间:2020年01月27日 - 12:38

最后更新:2020年01月27日 - 18:34

原始链接:https://gsy00517.github.io/ubuntu20200127123832/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:snap软件包管理及问题 | 高深远的博客

ubuntu笔记:snap软件包管理及问题

在我安装ubuntu18.04LTS的时候,由于下载语言包是真的久,我就翻了一下ubuntu安装界面的介绍,其中一开始就是对snap store的介绍。

这篇文章就说说snap和我之前遇到的问题。

References

电子文献:
https://www.jb51.net/article/128368.htm
https://blog.csdn.net/u011870280/article/details/80213866


snap

snap是ubuntu母公司Canonical于2016年4月发布ubuntu16.04时候引入的一种安全的、易于管理的、沙盒化的软件包格式,与传统的dpkg和apt有着很大的区别。在ubuntu软件中心下载安装的似乎都是snap管理的。这让一些商业闭源软件也能在linux上发布,说白了是ubuntu为了获得linux发行版霸权的一个重要举措,因此没少招黑。知乎上看到这么一句话,笑半天:“Fuck the political correct, make linux great again.(says 川·乌班图·普)”。


常用命令

  1. 列出已经安装的snap包

    1
    sudo snap list
  2. 搜索要安装的snap包

    1
    sudo snap find <text to search>
  3. 安装一个snap包

    1
    sudo snap install <snap name>
  4. 更新一个snap包

    1
    sudo snap refresh <snap name>

    注:如果后面不加包的名字就更新所有的snap包。

  5. 把一个包还原到以前安装的版本

    1
    sudo snap revert <snap name>
  6. 删除一个snap包

    1
    sudo snap remove <snap name>
  7. 查看最近的更改

    1
    snap changes
  8. 终止snap进程

    1
    sudo snap abort <进程序号>

后面两个命令将在下面的问题中发挥作用。


问题

当我在snap store也就是ubuntu软件中心下载pycharm和VScode时,遇到了如下报错:

1
snapd returned status code 409: Conflict

上网查找之后,才知道这个错误码409表示的是:由于和被请求的资源的当前状态之间存在冲突,请求无法完成。即并发执行时返回的错误码。
由于之前ubuntu软件中心无响应被我强制退出了,因此的确很有可能与之前进行到一半的安装冲突。于是使用snap changes查看最近的snap更改。

果然看见之前的snap进程依旧在“Doing”,因此根据对应的序号使用sudo snap abort终止进程。
这时再回到软件中心安装,就没有之前的报错了。
然而…


关于国内使用snap

因为网络原因,而且也没有可用的镜像,导致snapcraft在中国大陆地区访问速度非常非常慢,下载软件需要很长的时间并且很容易中途出错。
此外,由于snap软件会把主分区分成好多个loop,看起来真的不想说什么了。图源自贴吧,可以看到这挂载的snap软件包可以说是相当壮(别)观(扭)了。

还有一个杀死强迫症(比如我)的问题就是,snap会在家目录(即18.04的主文件夹)中创建一个snap文件夹,里面各种快捷方式、循环嵌套的文件夹,害…无法用语言描述,看了就知道,总之就是非常不爽。主要是一些资料、文件一般也会放在家目录下面,看到了snap在那边亮眼睛真的难受。
实在不知道为什么社区里有不少人推崇snap(不过国外没速度限制,snap对他们来说挺方便的)。
总而言之,综合速度(硬伤)和美观舒适度考虑,还是尽量避免使用snap命令安装软件,也不要下载ubuntu软件商店中的snap格式的软件包(基本都是)。甚至有些“安装ubuntu之后必做的…件事”等诸如此类的ubuntu配置或者美化的教程内直接把卸载snap列作其中一项哈哈。
总之管理软件还是apt优先,详见我的博文ubuntu笔记:apt包管理以及如何更新软件列表


碰到底线咯 后面没有啦

本文标题:ubuntu笔记:snap软件包管理及问题

文章作者:高深远

发布时间:2020年01月27日 - 12:38

最后更新:2020年01月27日 - 18:34

原始链接:https://gsy00517.github.io/ubuntu20200127123832/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:移动文件的妙用 | 高深远的博客

ubuntu笔记:移动文件的妙用

有时候会遇到文件无法重命名的问题,这里介绍一种很神奇的方法,亲测有效。


方法

  1. 首先,在终端把目录切到要重命名的文件目录下,或者直接在对应目录中打开终端。
  2. 接下来就是神奇的地方了,为了防止权限不够加个sudo赋个权。
    1
    sudo mv 原文件名 新文件名

碰到底线咯 后面没有啦

本文标题:ubuntu笔记:移动文件的妙用

文章作者:高深远

发布时间:2020年01月27日 - 18:43

最后更新:2020年01月27日 - 18:48

原始链接:https://gsy00517.github.io/ubuntu20200127184352/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:移动文件的妙用 | 高深远的博客

ubuntu笔记:移动文件的妙用

有时候会遇到文件无法重命名的问题,这里介绍一种很神奇的方法,亲测有效。


方法

  1. 首先,在终端把目录切到要重命名的文件目录下,或者直接在对应目录中打开终端。
  2. 接下来就是神奇的地方了,为了防止权限不够加个sudo赋个权。
    1
    sudo mv 原文件名 新文件名

碰到底线咯 后面没有啦

本文标题:ubuntu笔记:移动文件的妙用

文章作者:高深远

发布时间:2020年01月27日 - 18:43

最后更新:2020年01月27日 - 18:48

原始链接:https://gsy00517.github.io/ubuntu20200127184352/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:权限管理 | 高深远的博客

ubuntu笔记:权限管理

之前在windows下面从来没想过权限的事情,而在ubuntu中这点就很受重视,可谓时时都会遇到权限管理的问题。即使如此,搞崩ubuntu的次数还是比windows要多的,不过好像心也没那么痛,可能ubuntu就是拿来玩的。虽然不是每次搞崩都是因为权限,但还是有必要理一理。

References

参考文献:
[1]完美应用ubuntu(第3版)


权限

在linux系统中,我们可以在终端使用ls -l查看目录下所有子目录和文件的权限属性。其输出结果中每一列的含义如下:

  • 第一列:文件类型和权限。
  • 第二列:i节点,即硬链接数。
  • 第三列:文件的属主,即文件的所有者。
  • 第四列:文件的属组。
  • 第五列:文件的大小。
  • 第六列:mtime,即最后一次修改时间。
  • 第七列:文件或者目录名。

其实不做服务器的话没必要搞那么懂,我就讲一下我认为最重要的第一列。
首先,第一个字母表示的是文件类型,主要有下面几种:

  • -:表示普通文件。
  • d:表示目录。
  • l:表示链接文件。
  • b:表示块设备文件,比如硬盘的存储设备等。
  • c:表示字符设备文件,比如键盘。
  • s:表示套接字文件,主要跟网络程序有关。
  • p:表示管道文件。

其次,之后的九个字母三个为一组,分别表示的是文件所有者(u)的权限、同组用户(g)的权限和其他用户(o)的权限。这里属主一般就是sudo赋权进入的那个用户,一般在个人系统中就是特权用户root。另外,可以用“a”表示all users。
在每个三个字母组成的一组中,依次分别为读(r)、写(w)和执行(x)权限。若是字母,则表示可;若是“-”,则表示不可。例如“rw-”表示的是“可读可写不可执行”。
为了方便,还可以用数字代表权限:用4代表读权限,用2代表写权限,用1代表执行权限。可以发现,这样的三个数字之和(0-7)可以表示任何一种权限组合。
较为常用权限组合的有:

  • 7(可读可写可执行——rwx——4+2+1=7)
  • 6(可读可写不可执行——rw-——4+2+0=6)
  • 4(可读不可写不可执行——r———4+0+0=4)

chmod

一般通过chmod命令来修改权限,主要有两种方法。

  1. 数字法

    这种方法最简洁,其基本格式是chmod (-R) 模式 文件名。这里的-R可以用来进行多级目录的权限设定,也就是将指定文件夹内的所有文件都修改权限。
    以两个较为常用的使用为例。

    1
    2
    3
    4
    5
    sudo chmod 666 文件名
    #赋予所有用户读和写的权限,一般没有权限时我都会使用这个命令

    sudo chmod 600 文件名
    #赋予文件所有者读和写的权限,给group和other只读权限
  2. 参数法

    这种方法适用于只需要改变单个用户的权限而又不想考虑或者计算别的用户的权限情况,其基本格式是chmod [u/g/o/a] [+/-/=] (rwxst) 文件名
    这里先解释一下几个重要的参数和符号。
    u:所属用户。
    g:同组用户。
    o:其他用户。
    a:所有用户,相当于ugo。
    +:原权限基础上增加权限。
    -:原权限基础上减少权限。
    =:无论原权限是什么,最后的权限都修改为这里指定的权限。
    r:不解释,不懂的话没好好看前文。
    w:不解释,不懂的话没好好看前文。
    x:不解释,不懂的话没好好看前文。
    s:运行时可置UID。
    t:运行时可置GID。
    来看例子:

    1
    2
    3
    4
    5
    6
    7
    8
    sudo chmod u+rw 文件名
    #给用户增加读写权限

    sudo chmod o-rwx 文件名
    #不允许其他用户读写执行

    sudo chmod g=rx 文件名
    #使同组用户只能读和执行

碰到底线咯 后面没有啦

本文标题:ubuntu笔记:权限管理

文章作者:高深远

发布时间:2020年01月27日 - 18:51

最后更新:2020年01月28日 - 15:02

原始链接:https://gsy00517.github.io/ubuntu20200127185146/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
ubuntu笔记:权限管理 | 高深远的博客

ubuntu笔记:权限管理

之前在windows下面从来没想过权限的事情,而在ubuntu中这点就很受重视,可谓时时都会遇到权限管理的问题。即使如此,搞崩ubuntu的次数还是比windows要多的,不过好像心也没那么痛,可能ubuntu就是拿来玩的。虽然不是每次搞崩都是因为权限,但还是有必要理一理。

References

参考文献:
[1]完美应用ubuntu(第3版)


权限

在linux系统中,我们可以在终端使用ls -l查看目录下所有子目录和文件的权限属性。其输出结果中每一列的含义如下:

  • 第一列:文件类型和权限。
  • 第二列:i节点,即硬链接数。
  • 第三列:文件的属主,即文件的所有者。
  • 第四列:文件的属组。
  • 第五列:文件的大小。
  • 第六列:mtime,即最后一次修改时间。
  • 第七列:文件或者目录名。

其实不做服务器的话没必要搞那么懂,我就讲一下我认为最重要的第一列。
首先,第一个字母表示的是文件类型,主要有下面几种:

  • -:表示普通文件。
  • d:表示目录。
  • l:表示链接文件。
  • b:表示块设备文件,比如硬盘的存储设备等。
  • c:表示字符设备文件,比如键盘。
  • s:表示套接字文件,主要跟网络程序有关。
  • p:表示管道文件。

其次,之后的九个字母三个为一组,分别表示的是文件所有者(u)的权限、同组用户(g)的权限和其他用户(o)的权限。这里属主一般就是sudo赋权进入的那个用户,一般在个人系统中就是特权用户root。另外,可以用“a”表示all users。
在每个三个字母组成的一组中,依次分别为读(r)、写(w)和执行(x)权限。若是字母,则表示可;若是“-”,则表示不可。例如“rw-”表示的是“可读可写不可执行”。
为了方便,还可以用数字代表权限:用4代表读权限,用2代表写权限,用1代表执行权限。可以发现,这样的三个数字之和(0-7)可以表示任何一种权限组合。
较为常用权限组合的有:

  • 7(可读可写可执行——rwx——4+2+1=7)
  • 6(可读可写不可执行——rw-——4+2+0=6)
  • 4(可读不可写不可执行——r———4+0+0=4)

chmod

一般通过chmod命令来修改权限,主要有两种方法。

  1. 数字法

    这种方法最简洁,其基本格式是chmod (-R) 模式 文件名。这里的-R可以用来进行多级目录的权限设定,也就是将指定文件夹内的所有文件都修改权限。
    以两个较为常用的使用为例。

    1
    2
    3
    4
    5
    sudo chmod 666 文件名
    #赋予所有用户读和写的权限,一般没有权限时我都会使用这个命令

    sudo chmod 600 文件名
    #赋予文件所有者读和写的权限,给group和other只读权限
  2. 参数法

    这种方法适用于只需要改变单个用户的权限而又不想考虑或者计算别的用户的权限情况,其基本格式是chmod [u/g/o/a] [+/-/=] (rwxst) 文件名
    这里先解释一下几个重要的参数和符号。
    u:所属用户。
    g:同组用户。
    o:其他用户。
    a:所有用户,相当于ugo。
    +:原权限基础上增加权限。
    -:原权限基础上减少权限。
    =:无论原权限是什么,最后的权限都修改为这里指定的权限。
    r:不解释,不懂的话没好好看前文。
    w:不解释,不懂的话没好好看前文。
    x:不解释,不懂的话没好好看前文。
    s:运行时可置UID。
    t:运行时可置GID。
    来看例子:

    1
    2
    3
    4
    5
    6
    7
    8
    sudo chmod u+rw 文件名
    #给用户增加读写权限

    sudo chmod o-rwx 文件名
    #不允许其他用户读写执行

    sudo chmod g=rx 文件名
    #使同组用户只能读和执行

碰到底线咯 后面没有啦

本文标题:ubuntu笔记:权限管理

文章作者:高深远

发布时间:2020年01月27日 - 18:51

最后更新:2020年01月28日 - 15:02

原始链接:https://gsy00517.github.io/ubuntu20200127185146/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
verilog笔记:数值比较器实现与vivado使用 | 高深远的博客

verilog笔记:数值比较器实现与vivado使用

本周数字电路课程老师布置了一个利用verilog语言进行数值比较器波形仿真的作业。可以利用Modelsim或者Vivado实现。由于Vivado默认安装大小就有将近30个GB(2018版好像是27GB左右,2014版是12.68GB,这版本的容量增速跟maltab有得一拼啊),因此之前装了之后不太会使用便又卸了。最近刚好趁着双十一降价给自己的laptop加了一个SSD,因此正好赶快学习一下如何使用。有关如何给笔记本加装SSD的问题,这里有两个视频可以解决,安装准备与步骤安装后点亮磁盘

References

电子文献:
https://blog.csdn.net/qq_41154156/article/details/80989125
https://wenku.baidu.com/view/0294cbb3bb4cf7ec4bfed01a.html


关于vivado

相比于Modelsim,Vivado的UI还是要舒服许多的,有点像Multisim之于Pspice。关于Vivado的使用,上面参考的文章中的步骤比较详细,照做一遍之后基本就会了。


1位数值比较器

1位数值比较器的逻辑图如下:

使用verilog代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module _1bit_Comp(
input A,
input B,
output AGB,
output AEB,
output ALB
);
wire Anot, Bnot;
not n0(Anot, A),
n1(Bnot, B);
and n2(AGB, A, Bnot),
n3(ALB, Anot, B);
nor n4(AEB, AGB, ALB);
endmodule

为了输出仿真波形,新建一个仿真文件:

1
2
3
4
5
6
7
8
9
module simulateFile();
reg A, B;
wire AGB, AEB, ALB;
_1bit_Comp u1(A, B, AGB, AEB, ALB);
initial
begin A = 0; B = 0;
end
always #50 {A, B} = {A, B} + 1;
endmodule

其中,过程赋值语句always只能给寄存器类型变量赋值,因此,在这里A、B要定义为reg类型。
这里“#50”表示延时,使用{A, B}使AB变成二进制数,方便生成所有不同的输入,在这里即00、01、10、11。
Run Simulation,输出波形:


2位数值比较器

2位数值比较器的逻辑图如下:

使用verilog代码,调用1位数值比较器,实现2位数值比较器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module _2bit_Comp(
input A1,
input A0,
input B1,
input B0,
output FAGB,
output FAEB,
output FALB
);
wire AGB1, AEB1, ALB1, AGB0, AEB0, ALB0; //signal inside
wire G1O, G2O; //the output of and gate G1, G2
_1bit_Comp C1(A1, B1, AGB1, AEB1, ALB1); //Instantiate 1-bit Comparator
_1bit_Comp C0(A0, B0, AGB0, AEB0, ALB0);
and G1(G1O, AEB1, AGB0),
G2(G2O, AEB1, ALB0),
G3(FAEB, AEB1, AEB0);
or G4(FAGB, AGB1, G1O);
or G5(FALB, ALB1, G2O);
endmodule

可以使用RTL ANALYSIS来仿真出2位数值比较器的RTL schematic电子原理图。

类似的,编写仿真文件:

1
2
3
4
5
6
7
8
9
module simulateAgain();
reg A1, A0, B1, B0;
wire FAGB, FAEB, FALB;
_2bit_Comp u2(A1, A0, B1, B0, FAGB, FAEB, FALB);
initial
begin A1 = 0; A0 = 0; B1 = 0; B0 = 0;
end
always #30 {A1, A0, B1, B0} = {A1, A0, B1, B0} + 1;
endmodule

Run Simulation,输出波形:


出现的问题

  1. 报错:[Synth 8-439] module’xxx’not found

    当初遇到这个问题后,我的第一反应是上网搜索原因。得到的解释有模块未添加、IP未正确设置等。
    对照网上的解决方案之后,我发现除了我无法理解的,网上所述的问题我都不存在。于是我只好独立进行思考。
    果然,我还犯不了像网上那样“高级”的错误。错误的原代码如下:

    1
    2
    not n0(Anot, A);
    n1(Bnot, B);

    报错:[Synth 8-439] module’n1’not found。
    当我调用门的时候,由于内部变量换行导致我将逗号误用成了分号,因此导致分号之后的变量not found,修改后错误即可解决。
    其实,这个问题仔细观察即可发觉,相比于n1,同样格式的n0就没有报错,那么很有可能错误就在两者之间。

  2. ERROR: [Common 17-39] ‘xxx’ failed due to earlier errors

    这是我在执行仿真文件时遇到的error。仔细检查后,发现错误也与上一个问题相同。由于我在设计完电路后没有Run Synthesis综合并生成网表文件来进行检验,也没有进行其它的仿真操作,因此之前并没能发现这个问题。于是最后当调用该电路的仿真文件开始运行时就会报告这样的错误。

要注意的点

  1. 和matlab中函数文件的要求类似,verilog定义模块时,需要新建的模块文件名称与模块的文件名称一致。例如,我上面的1位数值比较器module名为_1bit_Comp,那么对应的文件名就应该是_1bit_Comp.v。此外,每个模块应使用一个文件来表示,且一个文件最多能表示一个模块(可以在其中调用其它模块,这点和matlab很像),两者呈一一对应关系。
  2. 新建project时,如要从RTL代码开始综合,就选择RTL project(默认的这个)。要注意的是,下面的“Do not specify sources at this time”(此时不定义源文件)可以勾上。否则,下一步会进入添加source file。
  3. 如果在一个project中已经建立了一个仿真文件,那么当你新建一个仿真文件时,需要建立在create的new file内,这样在后面对不同的仿真文件进行仿真时可以将对应的文件夹依次分别激活。
  4. 在source窗口中,一般情况下,Vivado会自动加粗识别出来的top module,同时对应module名称前面也会有一个二叉树状的图标表示这是顶层模块。有时候,软件也会识别错误或者与实际需求不符,这时候我们可以右键想要置顶的module,在弹出的菜单中点击Set As Top将其设为顶层。
  5. 当在同一个project中创建了多个仿真文件时,如要在进行完一次仿真之后对另一个仿真文件,需要对对应的文件夹进行激活。方法是右键仿真文件,然后在弹出的菜单中点击Make Active即可。

加装固态盘

前文提到给笔记本加装SSD,给了两个示范视频,这里我还是想再稍稍补充一下关于加装固态盘的一些事情。
首先必须确保自己的本有空位。我使用的是小电池版本,因此有一整个2.5英寸7mm的硬盘位,这个请在决定购买新的硬盘前和卖家自己核对确认。如果还是不放心,那么最好亲自拆开查看,眼见为实嘛。要注意的是,必须使用完全对应规格的螺丝刀(比如我使用的是梅花T5螺丝刀),否则很容易发生滑丝,即螺牙连接处由于受力过大或其它原因导致螺牙磨损而使螺牙无法咬合,这会为今后的拆机带来不必要的麻烦。


上面是我买的固态盘和数据线,相同或者类似机型的可以参考一下。在到货之后我发现,我所买的闪迪SSD较7mm稍薄些,因此平时拿动时(一般较大幅度翻动laptop时)会感到里面有东西松动的响声,不过使用至今没遇到任何问题。另外,为了适配各类硬盘,数据线的长度也有可能不能完全匹配,其实也没有关系,稍稍用力将数据线对应接头按入SATA3接口并用架子将固态盘固定即可。相比移动硬盘的USB接口,内置固态的SATA3读取速度还是相当不错的。


碰到底线咯 后面没有啦

本文标题:verilog笔记:数值比较器实现与vivado使用

文章作者:高深远

发布时间:2019年11月15日 - 23:31

最后更新:2020年01月19日 - 08:27

原始链接:https://gsy00517.github.io/verilog20191115233116/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
verilog笔记:数值比较器实现与vivado使用 | 高深远的博客

verilog笔记:数值比较器实现与vivado使用

本周数字电路课程老师布置了一个利用verilog语言进行数值比较器波形仿真的作业。可以利用Modelsim或者Vivado实现。由于Vivado默认安装大小就有将近30个GB(2018版好像是27GB左右,2014版是12.68GB,这版本的容量增速跟maltab有得一拼啊),因此之前装了之后不太会使用便又卸了。最近刚好趁着双十一降价给自己的laptop加了一个SSD,因此正好赶快学习一下如何使用。有关如何给笔记本加装SSD的问题,这里有两个视频可以解决,安装准备与步骤安装后点亮磁盘

References

电子文献:
https://blog.csdn.net/qq_41154156/article/details/80989125
https://wenku.baidu.com/view/0294cbb3bb4cf7ec4bfed01a.html


关于vivado

相比于Modelsim,Vivado的UI还是要舒服许多的,有点像Multisim之于Pspice。关于Vivado的使用,上面参考的文章中的步骤比较详细,照做一遍之后基本就会了。


1位数值比较器

1位数值比较器的逻辑图如下:

使用verilog代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module _1bit_Comp(
input A,
input B,
output AGB,
output AEB,
output ALB
);
wire Anot, Bnot;
not n0(Anot, A),
n1(Bnot, B);
and n2(AGB, A, Bnot),
n3(ALB, Anot, B);
nor n4(AEB, AGB, ALB);
endmodule

为了输出仿真波形,新建一个仿真文件:

1
2
3
4
5
6
7
8
9
module simulateFile();
reg A, B;
wire AGB, AEB, ALB;
_1bit_Comp u1(A, B, AGB, AEB, ALB);
initial
begin A = 0; B = 0;
end
always #50 {A, B} = {A, B} + 1;
endmodule

其中,过程赋值语句always只能给寄存器类型变量赋值,因此,在这里A、B要定义为reg类型。
这里“#50”表示延时,使用{A, B}使AB变成二进制数,方便生成所有不同的输入,在这里即00、01、10、11。
Run Simulation,输出波形:


2位数值比较器

2位数值比较器的逻辑图如下:

使用verilog代码,调用1位数值比较器,实现2位数值比较器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module _2bit_Comp(
input A1,
input A0,
input B1,
input B0,
output FAGB,
output FAEB,
output FALB
);
wire AGB1, AEB1, ALB1, AGB0, AEB0, ALB0; //signal inside
wire G1O, G2O; //the output of and gate G1, G2
_1bit_Comp C1(A1, B1, AGB1, AEB1, ALB1); //Instantiate 1-bit Comparator
_1bit_Comp C0(A0, B0, AGB0, AEB0, ALB0);
and G1(G1O, AEB1, AGB0),
G2(G2O, AEB1, ALB0),
G3(FAEB, AEB1, AEB0);
or G4(FAGB, AGB1, G1O);
or G5(FALB, ALB1, G2O);
endmodule

可以使用RTL ANALYSIS来仿真出2位数值比较器的RTL schematic电子原理图。

类似的,编写仿真文件:

1
2
3
4
5
6
7
8
9
module simulateAgain();
reg A1, A0, B1, B0;
wire FAGB, FAEB, FALB;
_2bit_Comp u2(A1, A0, B1, B0, FAGB, FAEB, FALB);
initial
begin A1 = 0; A0 = 0; B1 = 0; B0 = 0;
end
always #30 {A1, A0, B1, B0} = {A1, A0, B1, B0} + 1;
endmodule

Run Simulation,输出波形:


出现的问题

  1. 报错:[Synth 8-439] module’xxx’not found

    当初遇到这个问题后,我的第一反应是上网搜索原因。得到的解释有模块未添加、IP未正确设置等。
    对照网上的解决方案之后,我发现除了我无法理解的,网上所述的问题我都不存在。于是我只好独立进行思考。
    果然,我还犯不了像网上那样“高级”的错误。错误的原代码如下:

    1
    2
    not n0(Anot, A);
    n1(Bnot, B);

    报错:[Synth 8-439] module’n1’not found。
    当我调用门的时候,由于内部变量换行导致我将逗号误用成了分号,因此导致分号之后的变量not found,修改后错误即可解决。
    其实,这个问题仔细观察即可发觉,相比于n1,同样格式的n0就没有报错,那么很有可能错误就在两者之间。

  2. ERROR: [Common 17-39] ‘xxx’ failed due to earlier errors

    这是我在执行仿真文件时遇到的error。仔细检查后,发现错误也与上一个问题相同。由于我在设计完电路后没有Run Synthesis综合并生成网表文件来进行检验,也没有进行其它的仿真操作,因此之前并没能发现这个问题。于是最后当调用该电路的仿真文件开始运行时就会报告这样的错误。

要注意的点

  1. 和matlab中函数文件的要求类似,verilog定义模块时,需要新建的模块文件名称与模块的文件名称一致。例如,我上面的1位数值比较器module名为_1bit_Comp,那么对应的文件名就应该是_1bit_Comp.v。此外,每个模块应使用一个文件来表示,且一个文件最多能表示一个模块(可以在其中调用其它模块,这点和matlab很像),两者呈一一对应关系。
  2. 新建project时,如要从RTL代码开始综合,就选择RTL project(默认的这个)。要注意的是,下面的“Do not specify sources at this time”(此时不定义源文件)可以勾上。否则,下一步会进入添加source file。
  3. 如果在一个project中已经建立了一个仿真文件,那么当你新建一个仿真文件时,需要建立在create的new file内,这样在后面对不同的仿真文件进行仿真时可以将对应的文件夹依次分别激活。
  4. 在source窗口中,一般情况下,Vivado会自动加粗识别出来的top module,同时对应module名称前面也会有一个二叉树状的图标表示这是顶层模块。有时候,软件也会识别错误或者与实际需求不符,这时候我们可以右键想要置顶的module,在弹出的菜单中点击Set As Top将其设为顶层。
  5. 当在同一个project中创建了多个仿真文件时,如要在进行完一次仿真之后对另一个仿真文件,需要对对应的文件夹进行激活。方法是右键仿真文件,然后在弹出的菜单中点击Make Active即可。

加装固态盘

前文提到给笔记本加装SSD,给了两个示范视频,这里我还是想再稍稍补充一下关于加装固态盘的一些事情。
首先必须确保自己的本有空位。我使用的是小电池版本,因此有一整个2.5英寸7mm的硬盘位,这个请在决定购买新的硬盘前和卖家自己核对确认。如果还是不放心,那么最好亲自拆开查看,眼见为实嘛。要注意的是,必须使用完全对应规格的螺丝刀(比如我使用的是梅花T5螺丝刀),否则很容易发生滑丝,即螺牙连接处由于受力过大或其它原因导致螺牙磨损而使螺牙无法咬合,这会为今后的拆机带来不必要的麻烦。


上面是我买的固态盘和数据线,相同或者类似机型的可以参考一下。在到货之后我发现,我所买的闪迪SSD较7mm稍薄些,因此平时拿动时(一般较大幅度翻动laptop时)会感到里面有东西松动的响声,不过使用至今没遇到任何问题。另外,为了适配各类硬盘,数据线的长度也有可能不能完全匹配,其实也没有关系,稍稍用力将数据线对应接头按入SATA3接口并用架子将固态盘固定即可。相比移动硬盘的USB接口,内置固态的SATA3读取速度还是相当不错的。


碰到底线咯 后面没有啦

本文标题:verilog笔记:数值比较器实现与vivado使用

文章作者:高深远

发布时间:2019年11月15日 - 23:31

最后更新:2020年01月19日 - 08:27

原始链接:https://gsy00517.github.io/verilog20191115233116/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
verilog笔记:运动码表的硬件描述语言实现 | 高深远的博客

verilog笔记:运动码表的硬件描述语言实现

继上一篇logisim笔记:基本使用及运动码表的实现,我还使用了硬件描述语言对同样需求的运动码表进行了实现,那么就在这里一并也总结一下吧。

References

电子文献:
https://blog.csdn.net/leon_zeng0/article/details/78441871
https://www.cnblogs.com/douzi2/p/5147151.html
https://blog.csdn.net/FPGADesigner/article/details/82425612


整体设计

由于需求与使用Logisim实现时一致,因此我的设计思路也基本沿用上一篇博文中提到的方案。但是要注意的是,这里的000状态并不在作为按键抬起之后的中间态,而是进入系统时的一个默认初始状态。


Vivado中一些高亮的含义

在具体的代码之前,我还想先归纳一下本次实践过程中遇到的和发现的Vivado中一些高亮提醒的含义。

  1. 土黄色高亮

    土黄色高亮出现的原因主要可能是下面三种情况:
    1. 定义重复。
    2. 定义放在了调用处的后面(identifier used before its declaration)。
    3. 声明残缺(empty statement)。
  2. 蓝色高亮

    1. 含有undeclared symbol。
    2. 和上面土黄色高亮相搭配出现,有定义重复时指明重复定义的位置。

16位数值比较器

该模块用于比较两个16位二进制数的大小,以确定是否需要存入记录的数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module _16bit_Comp(
input [15:0] A,
input [15:0] B,
output reg Y
);

always @ (A or B)
begin
if(A < B)
Y <= 1;
else
Y <= 0;
end
endmodule


16位寄存器

TMRecord表示码表暂停时的读数,regRecord表示寄存器中已经存储的记录,初始值为9999。控制信号有使能信号和reset信号。当收到reset信号时,直接将记录改为9999。当有使能信号时,将TMRecord记录下来。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module _16bit_Reg(
input enable,
input [15:0] TMRecord,
input [15:0] regRecord,
input [15:0] maxDefault,
input reset,
input clock,
output reg [15:0] next_record
);

always @ (reset or enable)
begin
if(reset & enable)
next_record <= maxDefault;
else if(enable)
next_record <= TMRecord;
else
next_record <= regRecord;
end
endmodule


数码管显示驱动

将BCD码转化为7位二进制数,即对应7段数码管,用于显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module watchDrive(
input [3:0] BCD,
output reg [6:0] light
);

always @ (BCD)
begin
case(BCD)
0: light = 7'B0111111;
1: light = 7'B0001001;
2: light = 7'B1011110;
3: light = 7'B1011011;
4: light = 7'B1101001;
5: light = 7'B1110011;
6: light = 7'B1110111;
7: light = 7'B0011001;
8: light = 7'B1111111;
9: light = 7'B1111011;
default: light = 7'B0111111;
endcase
end
endmodule


顶层文件

该部分主要包括对各个变量的定义和初始化;状态转换,即共设计了5种状态,对应不同的功能,当按下不同按键时,选择对应的状态并作为次态;数码管的显示与进位,即对数码管4个位置依次改动,从低位开始计算,当进位时产生进位信号到下一位。当有重置信号(这里使用的是reset和start的上升沿)时清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
`timescale 1ns / 1ps
//top module
module runningwatch(
input start,
input stop,
input store,
input reset,
input CLK,
output reg TM_EN, //enable the timer
output reg SD_EN, //enable register
output reg DP_SEL, //the screen show which data
output reg [15:0] regRecord, //data the register storing
output reg [15:0] TMRecord, //timer data
output [6:0] tenmsLight, hunmsLight, onesLight, tensLight
);

reg [3:0] tenMS, hunMS, oneS, tenS; //four 4bit digits from small to high and each from 0 to 9
reg tenmsup, hunmsup, onesup; //the signals if the bigger digit than itself should add

//allocate state
parameter S0 = 3'B000;
//initial state
parameter S1 = 3'B001;
//TIMING:timer set as 00.00,record does not change,show timer,timer counts,TM_EN = 1, SD_EN = 0, DP_SEL = 0
parameter S2 = 3'B010;
//PAUSE:timer does not change,record does not change,show timer,timer does not count,TM_EN = 0, SD_EN = 0, DP_SEL = 0
parameter S3 = 3'B011;
//UPDATE:timer does not change,record set as timer,show register,timer does not count,TM_EN = 0, SD_EN = 1, DP_SEL = 1
parameter S4 = 3'B100;
//KEEP:timer does not change,record does not change,show register,timer does not count,TM_EN = 0, SD_EN = 0, DP_SEL = 1
parameter S5 = 3'B101;
//RESET:timer set as 00.00,record set as 99.99,show timer,timer does not count,TM_EN = 0, SD_EN = 1, DP_SEL = 0

reg [2:0] state;
reg [2:0] next_state;

//reg CLK;
initial //only run once
begin
regRecord = 16'B0010011100001111;
TMRecord = 16'B0000000000000000;

state = S0;

tenMS = 0; hunMS = 0; oneS = 0; tenS = 0;
end

//to judge if store the timer's data
wire new;
reg newRecord;
_16bit_Comp comparator(TMRecord, regRecord, new); //compare
always @ (new)
newRecord = new;

reg [15:0] MAX = 16'B0010011100001111;

//sequential logic part
always @ (posedge CLK) //update the state at each posedge
begin
state <= next_state;
end

//combinatory logic part
//state transform
always @ (state or start or stop or store or reset or newRecord)
begin
next_state = S0; //if not press the key, back to the initial state

case(state)

S0: //initial state
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S1: //TIMING
begin
TM_EN = 1; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S2: //PAUSE
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S3: //UPDATE
begin
TM_EN = 0; SD_EN = 1; DP_SEL = 1;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S4: //KEEP
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 1;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S5: //RESET
begin
TM_EN = 0; SD_EN = 1; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

default: begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end //default initial state
endcase
end

//reset to zero
always @ (posedge reset or posedge start)
begin
tenMS <= 0; hunMS <= 0; oneS <= 0; tenS <= 0;
TMRecord <= 0;
end

//the followings are which have stated before:
//reg [3:0] tenMS, hunMS, oneS, tenS are four 4bit digits from small to high and each from 0 to 9
//reg tenmsup, hunmsup, onesup are the signals if the bigger digit than itself should add
//timer, divide into four digits
//10ms
always @ (posedge CLK)
begin
if(TM_EN)
begin
TMRecord = TMRecord + 1;
if(tenMS < 9)
begin tenMS <= tenMS + 1; tenmsup <= 0; end
else
begin tenMS <= 0; tenmsup <= 1; end
end
end
//100ms
always @ (posedge tenmsup)
begin
if(TM_EN)
begin
if(hunMS < 9)
begin hunMS <= hunMS + 1; hunmsup <= 0; end
else
begin hunMS <= 0; hunmsup <= 1; end
end
end

//1s
always @ (posedge hunmsup)
begin
if(TM_EN)
begin
if(oneS < 9)
begin oneS <= oneS + 1; onesup <= 0; end
else
begin oneS <= 0; onesup <= 1; end
end
end

//10s
always @ (posedge onesup)
begin
if(TM_EN)
begin
if(tenS < 9)
tenS <= oneS + 1;
else
oneS <= 0;
end
end

//save to the register
wire [15:0] newReg;
_16bit_Reg register(SD_EN, TMRecord, regRecord, MAX, reset, CLK, newReg);
always @ (newReg)
regRecord = newReg;

//change BCD to tube lights
watchDrive TENms(tenMS, tenmsLight);
watchDrive HUNms(hunMS, hunmsLight);
watchDrive ONEs(oneS, onesLight);
watchDrive TENs(tenS, tensLight);

endmodule


仿真文件

最后我们还需要自行编写一个仿真文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
module simulateFile();
reg start, stop, store, reset;
wire TM_EN, SD_EN, DP_SEL;
wire [15:0] regRecord;
wire [15:0] TMRecord;
wire [6:0] tenmsLight;
wire [6:0] hunmsLight;
wire [6:0] onesLight;
wire [6:0] tensLight;
reg CLK;

runningwatch watch(start, stop, store, reset, CLK,
TM_EN, SD_EN, DP_SEL, regRecord, TMRecord,
tenmsLight, hunmsLight, onesLight, tensLight);
initial
begin
CLK = 0;
start = 0;
stop = 0;
store = 0;
reset = 0;
end

begin
always
//generate clock signal
forever #10 CLK = ~CLK;

//always #100
//{start, stop, store, reset} = 4'B0000;
//begin start = 0; stop = 0; store = 0; reset = 0; end
always
begin
#50
{start, stop, store, reset} = 4'B0001;
#50
{start, stop, store, reset} = 4'B1000;
#500
{start, stop, store, reset} = 4'B0100;
#50
{start, stop, store, reset} = 4'B0010;
#50
{start, stop, store, reset} = 4'B1000;
#50
{start, stop, store, reset} = 4'B0100;
#100
{start, stop, store, reset} = 4'B0010;
#50
{start, stop, store, reset} = 4'B0001;
#50
$stop; //stop simulation
end
end
endmodule


仿真结果

Run simulation,得到如下输出波形图。


遇到的问题

  1. 报错:[Synth 8-462] no clock signal specified in event control

    我原本直接在顶层文件中定义时钟并使用forever生成连续的时钟信号,结果出现了如上报错。
    在仿佛调整后,最后发现解决的办法是将时钟信号放在仿真文件里生成然后作为input输入到顶层文件。
  2. 使用模块的输出赋值时遇到问题

    这个问题的主要原因还是因为我对于verilog赋值规则以及变量性质的不熟悉,这里做一个小归纳:
    1. 给wire赋值必须用assign。
    2. 给reg赋值用always。
    3. 使用非阻塞赋值时,reg不能给wire赋值,反之则可以。
    4. 使用阻塞赋值时,reg可以给wire赋值,反之则不行。

碰到底线咯 后面没有啦

本文标题:verilog笔记:运动码表的硬件描述语言实现

文章作者:高深远

发布时间:2020年01月07日 - 21:11

最后更新:2020年01月19日 - 08:27

原始链接:https://gsy00517.github.io/verilog20200107211139/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
verilog笔记:运动码表的硬件描述语言实现 | 高深远的博客

verilog笔记:运动码表的硬件描述语言实现

继上一篇logisim笔记:基本使用及运动码表的实现,我还使用了硬件描述语言对同样需求的运动码表进行了实现,那么就在这里一并也总结一下吧。

References

电子文献:
https://blog.csdn.net/leon_zeng0/article/details/78441871
https://www.cnblogs.com/douzi2/p/5147151.html
https://blog.csdn.net/FPGADesigner/article/details/82425612


整体设计

由于需求与使用Logisim实现时一致,因此我的设计思路也基本沿用上一篇博文中提到的方案。但是要注意的是,这里的000状态并不在作为按键抬起之后的中间态,而是进入系统时的一个默认初始状态。


Vivado中一些高亮的含义

在具体的代码之前,我还想先归纳一下本次实践过程中遇到的和发现的Vivado中一些高亮提醒的含义。

  1. 土黄色高亮

    土黄色高亮出现的原因主要可能是下面三种情况:
    1. 定义重复。
    2. 定义放在了调用处的后面(identifier used before its declaration)。
    3. 声明残缺(empty statement)。
  2. 蓝色高亮

    1. 含有undeclared symbol。
    2. 和上面土黄色高亮相搭配出现,有定义重复时指明重复定义的位置。

16位数值比较器

该模块用于比较两个16位二进制数的大小,以确定是否需要存入记录的数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module _16bit_Comp(
input [15:0] A,
input [15:0] B,
output reg Y
);

always @ (A or B)
begin
if(A < B)
Y <= 1;
else
Y <= 0;
end
endmodule


16位寄存器

TMRecord表示码表暂停时的读数,regRecord表示寄存器中已经存储的记录,初始值为9999。控制信号有使能信号和reset信号。当收到reset信号时,直接将记录改为9999。当有使能信号时,将TMRecord记录下来。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module _16bit_Reg(
input enable,
input [15:0] TMRecord,
input [15:0] regRecord,
input [15:0] maxDefault,
input reset,
input clock,
output reg [15:0] next_record
);

always @ (reset or enable)
begin
if(reset & enable)
next_record <= maxDefault;
else if(enable)
next_record <= TMRecord;
else
next_record <= regRecord;
end
endmodule


数码管显示驱动

将BCD码转化为7位二进制数,即对应7段数码管,用于显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module watchDrive(
input [3:0] BCD,
output reg [6:0] light
);

always @ (BCD)
begin
case(BCD)
0: light = 7'B0111111;
1: light = 7'B0001001;
2: light = 7'B1011110;
3: light = 7'B1011011;
4: light = 7'B1101001;
5: light = 7'B1110011;
6: light = 7'B1110111;
7: light = 7'B0011001;
8: light = 7'B1111111;
9: light = 7'B1111011;
default: light = 7'B0111111;
endcase
end
endmodule


顶层文件

该部分主要包括对各个变量的定义和初始化;状态转换,即共设计了5种状态,对应不同的功能,当按下不同按键时,选择对应的状态并作为次态;数码管的显示与进位,即对数码管4个位置依次改动,从低位开始计算,当进位时产生进位信号到下一位。当有重置信号(这里使用的是reset和start的上升沿)时清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
`timescale 1ns / 1ps
//top module
module runningwatch(
input start,
input stop,
input store,
input reset,
input CLK,
output reg TM_EN, //enable the timer
output reg SD_EN, //enable register
output reg DP_SEL, //the screen show which data
output reg [15:0] regRecord, //data the register storing
output reg [15:0] TMRecord, //timer data
output [6:0] tenmsLight, hunmsLight, onesLight, tensLight
);

reg [3:0] tenMS, hunMS, oneS, tenS; //four 4bit digits from small to high and each from 0 to 9
reg tenmsup, hunmsup, onesup; //the signals if the bigger digit than itself should add

//allocate state
parameter S0 = 3'B000;
//initial state
parameter S1 = 3'B001;
//TIMING:timer set as 00.00,record does not change,show timer,timer counts,TM_EN = 1, SD_EN = 0, DP_SEL = 0
parameter S2 = 3'B010;
//PAUSE:timer does not change,record does not change,show timer,timer does not count,TM_EN = 0, SD_EN = 0, DP_SEL = 0
parameter S3 = 3'B011;
//UPDATE:timer does not change,record set as timer,show register,timer does not count,TM_EN = 0, SD_EN = 1, DP_SEL = 1
parameter S4 = 3'B100;
//KEEP:timer does not change,record does not change,show register,timer does not count,TM_EN = 0, SD_EN = 0, DP_SEL = 1
parameter S5 = 3'B101;
//RESET:timer set as 00.00,record set as 99.99,show timer,timer does not count,TM_EN = 0, SD_EN = 1, DP_SEL = 0

reg [2:0] state;
reg [2:0] next_state;

//reg CLK;
initial //only run once
begin
regRecord = 16'B0010011100001111;
TMRecord = 16'B0000000000000000;

state = S0;

tenMS = 0; hunMS = 0; oneS = 0; tenS = 0;
end

//to judge if store the timer's data
wire new;
reg newRecord;
_16bit_Comp comparator(TMRecord, regRecord, new); //compare
always @ (new)
newRecord = new;

reg [15:0] MAX = 16'B0010011100001111;

//sequential logic part
always @ (posedge CLK) //update the state at each posedge
begin
state <= next_state;
end

//combinatory logic part
//state transform
always @ (state or start or stop or store or reset or newRecord)
begin
next_state = S0; //if not press the key, back to the initial state

case(state)

S0: //initial state
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S1: //TIMING
begin
TM_EN = 1; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S2: //PAUSE
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S3: //UPDATE
begin
TM_EN = 0; SD_EN = 1; DP_SEL = 1;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S4: //KEEP
begin
TM_EN = 0; SD_EN = 0; DP_SEL = 1;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

S5: //RESET
begin
TM_EN = 0; SD_EN = 1; DP_SEL = 0;
if(start) begin next_state = S1; TM_EN = 1; SD_EN = 0; DP_SEL = 0; end
else if(stop) begin next_state = S2; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
else if(store & newRecord) begin next_state = S3; TM_EN = 0; SD_EN = 1; DP_SEL = 1; end
else if(store & ~newRecord) begin next_state = S4; TM_EN = 0; SD_EN = 0; DP_SEL = 1; end
else if(reset) begin next_state = S5; TM_EN = 0; SD_EN = 1; DP_SEL = 0; end
else begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end
end

default: begin next_state = S0; TM_EN = 0; SD_EN = 0; DP_SEL = 0; end //default initial state
endcase
end

//reset to zero
always @ (posedge reset or posedge start)
begin
tenMS <= 0; hunMS <= 0; oneS <= 0; tenS <= 0;
TMRecord <= 0;
end

//the followings are which have stated before:
//reg [3:0] tenMS, hunMS, oneS, tenS are four 4bit digits from small to high and each from 0 to 9
//reg tenmsup, hunmsup, onesup are the signals if the bigger digit than itself should add
//timer, divide into four digits
//10ms
always @ (posedge CLK)
begin
if(TM_EN)
begin
TMRecord = TMRecord + 1;
if(tenMS < 9)
begin tenMS <= tenMS + 1; tenmsup <= 0; end
else
begin tenMS <= 0; tenmsup <= 1; end
end
end
//100ms
always @ (posedge tenmsup)
begin
if(TM_EN)
begin
if(hunMS < 9)
begin hunMS <= hunMS + 1; hunmsup <= 0; end
else
begin hunMS <= 0; hunmsup <= 1; end
end
end

//1s
always @ (posedge hunmsup)
begin
if(TM_EN)
begin
if(oneS < 9)
begin oneS <= oneS + 1; onesup <= 0; end
else
begin oneS <= 0; onesup <= 1; end
end
end

//10s
always @ (posedge onesup)
begin
if(TM_EN)
begin
if(tenS < 9)
tenS <= oneS + 1;
else
oneS <= 0;
end
end

//save to the register
wire [15:0] newReg;
_16bit_Reg register(SD_EN, TMRecord, regRecord, MAX, reset, CLK, newReg);
always @ (newReg)
regRecord = newReg;

//change BCD to tube lights
watchDrive TENms(tenMS, tenmsLight);
watchDrive HUNms(hunMS, hunmsLight);
watchDrive ONEs(oneS, onesLight);
watchDrive TENs(tenS, tensLight);

endmodule


仿真文件

最后我们还需要自行编写一个仿真文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
module simulateFile();
reg start, stop, store, reset;
wire TM_EN, SD_EN, DP_SEL;
wire [15:0] regRecord;
wire [15:0] TMRecord;
wire [6:0] tenmsLight;
wire [6:0] hunmsLight;
wire [6:0] onesLight;
wire [6:0] tensLight;
reg CLK;

runningwatch watch(start, stop, store, reset, CLK,
TM_EN, SD_EN, DP_SEL, regRecord, TMRecord,
tenmsLight, hunmsLight, onesLight, tensLight);
initial
begin
CLK = 0;
start = 0;
stop = 0;
store = 0;
reset = 0;
end

begin
always
//generate clock signal
forever #10 CLK = ~CLK;

//always #100
//{start, stop, store, reset} = 4'B0000;
//begin start = 0; stop = 0; store = 0; reset = 0; end
always
begin
#50
{start, stop, store, reset} = 4'B0001;
#50
{start, stop, store, reset} = 4'B1000;
#500
{start, stop, store, reset} = 4'B0100;
#50
{start, stop, store, reset} = 4'B0010;
#50
{start, stop, store, reset} = 4'B1000;
#50
{start, stop, store, reset} = 4'B0100;
#100
{start, stop, store, reset} = 4'B0010;
#50
{start, stop, store, reset} = 4'B0001;
#50
$stop; //stop simulation
end
end
endmodule


仿真结果

Run simulation,得到如下输出波形图。


遇到的问题

  1. 报错:[Synth 8-462] no clock signal specified in event control

    我原本直接在顶层文件中定义时钟并使用forever生成连续的时钟信号,结果出现了如上报错。
    在仿佛调整后,最后发现解决的办法是将时钟信号放在仿真文件里生成然后作为input输入到顶层文件。
  2. 使用模块的输出赋值时遇到问题

    这个问题的主要原因还是因为我对于verilog赋值规则以及变量性质的不熟悉,这里做一个小归纳:
    1. 给wire赋值必须用assign。
    2. 给reg赋值用always。
    3. 使用非阻塞赋值时,reg不能给wire赋值,反之则可以。
    4. 使用阻塞赋值时,reg可以给wire赋值,反之则不行。

碰到底线咯 后面没有啦

本文标题:verilog笔记:运动码表的硬件描述语言实现

文章作者:高深远

发布时间:2020年01月07日 - 21:11

最后更新:2020年01月19日 - 08:27

原始链接:https://gsy00517.github.io/verilog20200107211139/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
verilog笔记:频率计实现与verilog中timescale的解释 | 高深远的博客

verilog笔记:频率计实现与verilog中timescale的解释

之前每次创建完一个新的文件,文件最上方总会显示一行timescale。当初觉得没什么作用,删了之后依旧没有任何问题便没把它当回事,直到后来做频率计数器的时候我才决定一探究竟。那么这篇文章也就一并谈一下这个问题。

References

电子文献:
https://blog.csdn.net/m0_37652453/article/details/90301902
https://blog.csdn.net/ciscomonkey/article/details/83661395
https://blog.csdn.net/qq_16923717/article/details/81099833


基本原理

一个简易的频率计数器主要由分频器和计数器构成,其基本原理就是记录由分频器得到的一段时间内被测信号上升沿的个数,从而求得被测信号的频率。


控制信号转换模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module control(
output reg Cnt_EN, //enable the counter count, so that the counting period can be controled
output wire Cnt_CR, //clear the counter every time when the measure begins
output wire Latch_Sig, //at its posedge, the value of the counter will be stored/latched
input nRST, //system reset signal
input CP //1Hz standard clock signal
);
always @ (posedge CP or negedge nRST)
begin
if(~nRST) //generate enable counting signal
Cnt_EN = 1'b0; //don't count
else
Cnt_EN = ~Cnt_EN; //two frequency divider for the clock signal
end
assign Latch_Sig = ~Cnt_EN; //generate latch signal
assign Cnt_CR = nRST & (~CP & Latch_Sig); //generate the clear signal for the counter
endmodule

计数模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module counter(
output reg [3:0] Q,
input CR, EN, CP
);
always @ (posedge CP or posedge CR)
begin
if(CR)
Q <= 4'b0000; //reset to zero
else if(~EN)
Q <= Q; //stop counting
else if(Q == 4'b1001)
Q <= 4'b0000;
else
Q <= Q + 1'b1; //counting, plus one
end
endmodule

寄存模块

1
2
3
4
5
6
7
8
9
10
11
12
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module Latch(
output reg [15:0] Qout,
input [15:0] Din,
input Load, CR
);
always @ (posedge Load or posedge CR)
if(CR)
Qout <= 16'h0000; //reset to zero first
else
Qout <= Din;
endmodule

顶层文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module Fre_Measure(
output wire [15:0] BCD, //transfer to display part
input _1HzIN, SigIN, nRST_Key,
output wire Cnt_EN, Cnt_CR, //control signals of the counter
output wire Latch_Sig, //latch singal
output wire [15:0] Cnt //8421BCDcode output
);

//call control block
control U0(Cnt_EN, Cnt_CR, Latch_Sig, nRST_Key, _1HzIN);

//measure counter
counter U1(Cnt[3:0], Cnt_CR, Cnt_EN, SigIN);
counter U2(Cnt[7:4], Cnt_CR, Cnt_EN, ~(Cnt[3:0] == 4'h9));
counter U3(Cnt[11:8], Cnt_CR, Cnt_EN, ~(Cnt[7:0] == 8'h99));
counter U4(Cnt[15:12], Cnt_CR, Cnt_EN, ~(Cnt[11:0] == 12'h999));

//call latch block
Latch U5(BCD, Cnt, Latch_Sig, ~nRST_Key);

endmodule

仿真文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module simulateFile();

wire [15:0] BCD; //transfer to display part
wire [15:0] Cnt; //8421BCDcode output
reg CLK, RST, Signal;
parameter T1 = 0.1, //100Hz
T2 = 0.01, //1000Hz
T3 = 0.002; //5000Hz
wire Cnt_EN, Cnt_CR; //control signals of the counter
wire Latch_Sig; //latch singal

Fre_Measure Watch(BCD, CLK, Signal, RST, Cnt_EN, Cnt_CR, Latch_Sig, Cnt);

initial
begin
RST = 0;
CLK = 0;
Signal = 0;
end

always
forever #10 CLK = ~CLK; //generate clock signal

always #T1 Signal = ~Signal; //T1 or T2 or T3

always
begin
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
end

endmodule

仿真结果

以100Hz(T1)为例,输出的仿真结果如下。

其中CLK是固定的1Hz基准时钟信号;RST是系统需求的复位按键,当按下复位即RST为下降沿时,可以看到计数器Cnt被清零同时第一排译码输出的BCD码也被清零;Signal为输入的信号,这里我使用的是100Hz的,由我自己设定,由于频率较快,可以看到波形图非常密集;Cnt_EN是计数使能信号,可见在它为高电平时,Cnt随着输入信号一样快速计数;Cnt_CR是清零信号,在每次计数使能的上升沿或者复位的下降沿到来时Cnt_CR置零,也就是对Cnt清零操作;此外,当时钟信号到来时,假如系统不在计数(Cnt_EN=0),那么Latch_Sig将置1,也就是把记录数值存入锁存器。
实际上,这种设计方案会存在±1的计数误差,应为输入信号不一定与分频器同周期,即有可能每次测量的起始位置出于输入信号一个周期内的不同状态。


timescale

timescale是Verilog中的预编译指令,指定位于它后边的module的时间单位和时间精度,直到遇到新的timescale指令或者resetall指令。它的语法如下:

1
`timescale time_unit / time_precision

假如我们延时x个time_unit,那延时的总时间time = x * time_unit,但最后真正延时的时间是根据time_precision对time进行四舍五入后的结果。

注意:

  1. time_unit和time_precision只能是1、10和100这三种整数,单位有s、ms、us、ns、ps和fs。
  2. time_precision必须小于等于time_unit。
  3. timescale的时间精度设置是会影响仿真时间的,1ps精度可能是1ns精度仿真时间的一倍还多,并且占用更多的内存,所以如果没有必要,应尽量将时间精度设置得更大一些。

仿真时间

之前进行仿真时,我往往是让它自动运行至系统默认时间然后停止,这就会造成出现好几次重复循环的情况。
在Vivado中,窗口上方有三个类似播放器中的按钮,从左往右依次是:复位、不停运行、按指定时长(在后面的栏中设定)运行。
此外,如果计算不准时间,可以直接在仿真文件末尾或者想要结束的地方使用$stop或者$finifsh来终止仿真。


碰到底线咯 后面没有啦

本文标题:verilog笔记:频率计实现与verilog中timescale的解释

文章作者:高深远

发布时间:2020年01月07日 - 21:54

最后更新:2020年01月19日 - 08:27

原始链接:https://gsy00517.github.io/verilog20200107215404/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
verilog笔记:频率计实现与verilog中timescale的解释 | 高深远的博客

verilog笔记:频率计实现与verilog中timescale的解释

之前每次创建完一个新的文件,文件最上方总会显示一行timescale。当初觉得没什么作用,删了之后依旧没有任何问题便没把它当回事,直到后来做频率计数器的时候我才决定一探究竟。那么这篇文章也就一并谈一下这个问题。

References

电子文献:
https://blog.csdn.net/m0_37652453/article/details/90301902
https://blog.csdn.net/ciscomonkey/article/details/83661395
https://blog.csdn.net/qq_16923717/article/details/81099833


基本原理

一个简易的频率计数器主要由分频器和计数器构成,其基本原理就是记录由分频器得到的一段时间内被测信号上升沿的个数,从而求得被测信号的频率。


控制信号转换模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module control(
output reg Cnt_EN, //enable the counter count, so that the counting period can be controled
output wire Cnt_CR, //clear the counter every time when the measure begins
output wire Latch_Sig, //at its posedge, the value of the counter will be stored/latched
input nRST, //system reset signal
input CP //1Hz standard clock signal
);
always @ (posedge CP or negedge nRST)
begin
if(~nRST) //generate enable counting signal
Cnt_EN = 1'b0; //don't count
else
Cnt_EN = ~Cnt_EN; //two frequency divider for the clock signal
end
assign Latch_Sig = ~Cnt_EN; //generate latch signal
assign Cnt_CR = nRST & (~CP & Latch_Sig); //generate the clear signal for the counter
endmodule

计数模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module counter(
output reg [3:0] Q,
input CR, EN, CP
);
always @ (posedge CP or posedge CR)
begin
if(CR)
Q <= 4'b0000; //reset to zero
else if(~EN)
Q <= Q; //stop counting
else if(Q == 4'b1001)
Q <= 4'b0000;
else
Q <= Q + 1'b1; //counting, plus one
end
endmodule

寄存模块

1
2
3
4
5
6
7
8
9
10
11
12
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module Latch(
output reg [15:0] Qout,
input [15:0] Din,
input Load, CR
);
always @ (posedge Load or posedge CR)
if(CR)
Qout <= 16'h0000; //reset to zero first
else
Qout <= Din;
endmodule

顶层文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module Fre_Measure(
output wire [15:0] BCD, //transfer to display part
input _1HzIN, SigIN, nRST_Key,
output wire Cnt_EN, Cnt_CR, //control signals of the counter
output wire Latch_Sig, //latch singal
output wire [15:0] Cnt //8421BCDcode output
);

//call control block
control U0(Cnt_EN, Cnt_CR, Latch_Sig, nRST_Key, _1HzIN);

//measure counter
counter U1(Cnt[3:0], Cnt_CR, Cnt_EN, SigIN);
counter U2(Cnt[7:4], Cnt_CR, Cnt_EN, ~(Cnt[3:0] == 4'h9));
counter U3(Cnt[11:8], Cnt_CR, Cnt_EN, ~(Cnt[7:0] == 8'h99));
counter U4(Cnt[15:12], Cnt_CR, Cnt_EN, ~(Cnt[11:0] == 12'h999));

//call latch block
Latch U5(BCD, Cnt, Latch_Sig, ~nRST_Key);

endmodule

仿真文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
`timescale 1ns / 1ps //unit 1ns, precision 1ps
module simulateFile();

wire [15:0] BCD; //transfer to display part
wire [15:0] Cnt; //8421BCDcode output
reg CLK, RST, Signal;
parameter T1 = 0.1, //100Hz
T2 = 0.01, //1000Hz
T3 = 0.002; //5000Hz
wire Cnt_EN, Cnt_CR; //control signals of the counter
wire Latch_Sig; //latch singal

Fre_Measure Watch(BCD, CLK, Signal, RST, Cnt_EN, Cnt_CR, Latch_Sig, Cnt);

initial
begin
RST = 0;
CLK = 0;
Signal = 0;
end

always
forever #10 CLK = ~CLK; //generate clock signal

always #T1 Signal = ~Signal; //T1 or T2 or T3

always
begin
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
#200 RST = 0;
#10 RST = 1;
end

endmodule

仿真结果

以100Hz(T1)为例,输出的仿真结果如下。

其中CLK是固定的1Hz基准时钟信号;RST是系统需求的复位按键,当按下复位即RST为下降沿时,可以看到计数器Cnt被清零同时第一排译码输出的BCD码也被清零;Signal为输入的信号,这里我使用的是100Hz的,由我自己设定,由于频率较快,可以看到波形图非常密集;Cnt_EN是计数使能信号,可见在它为高电平时,Cnt随着输入信号一样快速计数;Cnt_CR是清零信号,在每次计数使能的上升沿或者复位的下降沿到来时Cnt_CR置零,也就是对Cnt清零操作;此外,当时钟信号到来时,假如系统不在计数(Cnt_EN=0),那么Latch_Sig将置1,也就是把记录数值存入锁存器。
实际上,这种设计方案会存在±1的计数误差,应为输入信号不一定与分频器同周期,即有可能每次测量的起始位置出于输入信号一个周期内的不同状态。


timescale

timescale是Verilog中的预编译指令,指定位于它后边的module的时间单位和时间精度,直到遇到新的timescale指令或者resetall指令。它的语法如下:

1
`timescale time_unit / time_precision

假如我们延时x个time_unit,那延时的总时间time = x * time_unit,但最后真正延时的时间是根据time_precision对time进行四舍五入后的结果。

注意:

  1. time_unit和time_precision只能是1、10和100这三种整数,单位有s、ms、us、ns、ps和fs。
  2. time_precision必须小于等于time_unit。
  3. timescale的时间精度设置是会影响仿真时间的,1ps精度可能是1ns精度仿真时间的一倍还多,并且占用更多的内存,所以如果没有必要,应尽量将时间精度设置得更大一些。

仿真时间

之前进行仿真时,我往往是让它自动运行至系统默认时间然后停止,这就会造成出现好几次重复循环的情况。
在Vivado中,窗口上方有三个类似播放器中的按钮,从左往右依次是:复位、不停运行、按指定时长(在后面的栏中设定)运行。
此外,如果计算不准时间,可以直接在仿真文件末尾或者想要结束的地方使用$stop或者$finifsh来终止仿真。


碰到底线咯 后面没有啦

本文标题:verilog笔记:频率计实现与verilog中timescale的解释

文章作者:高深远

发布时间:2020年01月07日 - 21:54

最后更新:2020年01月19日 - 08:27

原始链接:https://gsy00517.github.io/verilog20200107215404/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
vot toolkit笔记:解决无法连接TraX支持的问题 | 高深远的博客

vot toolkit笔记:解决无法连接TraX支持的问题

今天终于把vot toolkit给搞好了,能够跑通NCC了,简直不要太开心。先是在windows下问题比较多,后来换到ubuntu上,的确问题少了很多,也方便了很多。但是每次执行run_experiments或者run_test的时候,会出现如下报错:

1
2
3
4
5
6
7
8
9
10
Initializing workspace ...
Verifying native components ...
Testing TraX protocol support for tracker NCC.
Tracker execution interrupted: Unable to establish connection.
TraX support not detected.
错误使用 tracker_load (line 127)
Tracker has not passed the TraX support test.

出错 run_test (line 8)
tracker = tracker_load('NCC');

于是我打印了厚厚一摞的TraX Documentation,照着一些步骤作了一遍可依然没有效果,最后终于在vot toolkit的gitHubissue中找到了解决方法。

References

电子文献:
https://github.com/votchallenge/vot-toolkit/issues/201
https://github.com/votchallenge/vot-toolkit/issues/216
http://votchallenge.net/howto/perfeval.html


solution

首先说明,我的运行环境为ubuntu18.04与matlab2018b。
vot-toolkit/tracker/tracker_run.m文件中找到如下代码(大约在30至40行左右)。

1
2
3
4
% Hint to tracker that it should use trax
environment.TRAX = '1';

connection = 'standard';

修改为:

1
2
3
4
% Hint to tracker that it should use trax
environment.TRAX = '1';

connection = 'socket';

再次执行run_experiments或者run_test即可。
其实不是很清楚为什么work了,根据这段代码之后的条件判断语句可知在windows下必须使用socket连接。

注:由于这期间我也做了其他的一些调整,不保证直接修改上述代码之后一定能够成功。请先按上面所说修改,如果成功,那么接下来的部分可以跳过。

  1. 如果仅下载了vot toolkit直接执行的话,它会自动为你下载TraX。这里应该没有问题,如果担心的话可以自己去github下载TraX,然后复制到vot-toolkit/native/目录下(没有的话创建一个)。
  2. 来到vot-toolkit/native/trax目录下,接下来都是终端操作。
    • mkdir build
    • cd build
    • make ..(请确保已经安装好cmake)
    • sudo make install(不加sudo会出现权限问题)
  3. 如果完成上面之后,执行还有问题,可能是路径没有设置对。打开vot-toolkit/workspace/tracker_XXX.m(XXX是你创建工作区时设置的跟踪算法的名称)。找到最后一行:% tracker_linkpath = {}; % A cell array of custom library directories used by the tracker executable (optional)
    去掉前面的注释符,添加路径tracker_linkpath = {'absolute_path/trax/build'};(记得修改这里的absolute_path)。
    据TraX的作者所说,TraX出错一般不是vot toolkit本身的问题,如果你使用的是其他的算法,请确保该算法的.m或者.py文件和vot.m或者vot.py文件处在同一个目录下。如果没有,可到vot-toolkit/tracker/examples中的matlab或者python目录下复制。

TraX

折腾了半天,总得知道这个TraX是个什么东西,根据TraX文档中所说:TraX stands for visual Tracking eXchange, the protocol was designed to make development and testing of visual tracking algorithms simpler and faster.
其实我当初折腾的时候想法是:我不要simpler and faster,我现在只想跑通哈哈哈哈哈。


NCC

一般使用vot toolkit都会根据官方文档先跑一下NCC来看看有没有设置好。NCC(归一化互相关)是一种基于统计学计算两组样本数据相关性的算法,比较老,性能可想而知。


sequences

我和我的同学在运行的时候都卡在了下载数据集那里,尽管在下载,但是网络下行却一直显示为0。询问一位计科大佬之后知道在maltab中下载数据集是基本没有速度的,所以一般的建议是使用别人网盘中下载好的数据集,我已经上传到我的百度网盘,可通过此链接用提取码0glq下载。

注:数据集已经压缩成zip文件,但大小仍有1.67GB。

下载解压之后,打开可以看到一个list文本文件和许多序列文件夹(一个序列一个文件夹),可以通过修改list文本文件来确定要读取并进行实验的序列。

注:
执行run_test可以选择list文本文件中列出的序列,选择对应序号之后可以看到按帧实验的情况。
执行run_experiments会执行list文件中列出的所有序列,可以ctrl+C暂停,之后再次执行run_experiments会从上一次暂停的地方开始执行。done之后可以执行run_pack生成可以提交至VOT challenge的archive档案。


碰到底线咯 后面没有啦

本文标题:vot toolkit笔记:解决无法连接TraX支持的问题

文章作者:高深远

发布时间:2020年02月15日 - 18:52

最后更新:2020年05月02日 - 22:49

原始链接:https://gsy00517.github.io/vot-toolkit20200215185238/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
vot toolkit笔记:解决无法连接TraX支持的问题 | 高深远的博客

vot toolkit笔记:解决无法连接TraX支持的问题

今天终于把vot toolkit给搞好了,能够跑通NCC了,简直不要太开心。先是在windows下问题比较多,后来换到ubuntu上,的确问题少了很多,也方便了很多。但是每次执行run_experiments或者run_test的时候,会出现如下报错:

1
2
3
4
5
6
7
8
9
10
Initializing workspace ...
Verifying native components ...
Testing TraX protocol support for tracker NCC.
Tracker execution interrupted: Unable to establish connection.
TraX support not detected.
错误使用 tracker_load (line 127)
Tracker has not passed the TraX support test.

出错 run_test (line 8)
tracker = tracker_load('NCC');

于是我打印了厚厚一摞的TraX Documentation,照着一些步骤作了一遍可依然没有效果,最后终于在vot toolkit的gitHubissue中找到了解决方法。

References

电子文献:
https://github.com/votchallenge/vot-toolkit/issues/201
https://github.com/votchallenge/vot-toolkit/issues/216
http://votchallenge.net/howto/perfeval.html


solution

首先说明,我的运行环境为ubuntu18.04与matlab2018b。
vot-toolkit/tracker/tracker_run.m文件中找到如下代码(大约在30至40行左右)。

1
2
3
4
% Hint to tracker that it should use trax
environment.TRAX = '1';

connection = 'standard';

修改为:

1
2
3
4
% Hint to tracker that it should use trax
environment.TRAX = '1';

connection = 'socket';

再次执行run_experiments或者run_test即可。
其实不是很清楚为什么work了,根据这段代码之后的条件判断语句可知在windows下必须使用socket连接。

注:由于这期间我也做了其他的一些调整,不保证直接修改上述代码之后一定能够成功。请先按上面所说修改,如果成功,那么接下来的部分可以跳过。

  1. 如果仅下载了vot toolkit直接执行的话,它会自动为你下载TraX。这里应该没有问题,如果担心的话可以自己去github下载TraX,然后复制到vot-toolkit/native/目录下(没有的话创建一个)。
  2. 来到vot-toolkit/native/trax目录下,接下来都是终端操作。
    • mkdir build
    • cd build
    • make ..(请确保已经安装好cmake)
    • sudo make install(不加sudo会出现权限问题)
  3. 如果完成上面之后,执行还有问题,可能是路径没有设置对。打开vot-toolkit/workspace/tracker_XXX.m(XXX是你创建工作区时设置的跟踪算法的名称)。找到最后一行:% tracker_linkpath = {}; % A cell array of custom library directories used by the tracker executable (optional)
    去掉前面的注释符,添加路径tracker_linkpath = {'absolute_path/trax/build'};(记得修改这里的absolute_path)。
    据TraX的作者所说,TraX出错一般不是vot toolkit本身的问题,如果你使用的是其他的算法,请确保该算法的.m或者.py文件和vot.m或者vot.py文件处在同一个目录下。如果没有,可到vot-toolkit/tracker/examples中的matlab或者python目录下复制。

TraX

折腾了半天,总得知道这个TraX是个什么东西,根据TraX文档中所说:TraX stands for visual Tracking eXchange, the protocol was designed to make development and testing of visual tracking algorithms simpler and faster.
其实我当初折腾的时候想法是:我不要simpler and faster,我现在只想跑通哈哈哈哈哈。


NCC

一般使用vot toolkit都会根据官方文档先跑一下NCC来看看有没有设置好。NCC(归一化互相关)是一种基于统计学计算两组样本数据相关性的算法,比较老,性能可想而知。


sequences

我和我的同学在运行的时候都卡在了下载数据集那里,尽管在下载,但是网络下行却一直显示为0。询问一位计科大佬之后知道在maltab中下载数据集是基本没有速度的,所以一般的建议是使用别人网盘中下载好的数据集,我已经上传到我的百度网盘,可通过此链接用提取码0glq下载。

注:数据集已经压缩成zip文件,但大小仍有1.67GB。

下载解压之后,打开可以看到一个list文本文件和许多序列文件夹(一个序列一个文件夹),可以通过修改list文本文件来确定要读取并进行实验的序列。

注:
执行run_test可以选择list文本文件中列出的序列,选择对应序号之后可以看到按帧实验的情况。
执行run_experiments会执行list文件中列出的所有序列,可以ctrl+C暂停,之后再次执行run_experiments会从上一次暂停的地方开始执行。done之后可以执行run_pack生成可以提交至VOT challenge的archive档案。


碰到底线咯 后面没有啦

本文标题:vot toolkit笔记:解决无法连接TraX支持的问题

文章作者:高深远

发布时间:2020年02月15日 - 18:52

最后更新:2020年05月03日 - 09:33

原始链接:https://gsy00517.github.io/vot-toolkit20200215185238/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
windows笔记:释放空间 | 高深远的博客

windows笔记:释放空间

暑假里想跑CVPR中的代码,发现作者提供的环境配置都是基于linux终端的,这样windows的git bash就满足不了我了。二话不说我花了两天时间装了个ubuntu+windows双系统,好不容易装好了,却发现我的硬盘空间已经岌岌可危(理论上要留内存的三倍左右可以保证系统顺畅运行,我的内存是8G,也就是说我C盘应空出20G左右为宜)。于是我就找了些释放空间的办法,分享在这里。

References

电子文献:
http://www.udaxia.com/wtjd/9147.html


利用磁盘属性进行清理

这是最稳的方法,但释放的空间也相对较少,不过还是有效的。选择“此电脑”,右键C盘,属性,然后就可以在常规下面看到磁盘清理。一般按默认选择的进行清理,当然全点上勾也无所谓。

注意:千万不能选择压缩此驱动器以节约磁盘空间!

另外在工具下面你可以看到一个优化的选项,一般系统会定期自动执行优化,如果你是强迫症,时时刻刻都容不得一点冗余的话,可以手动优化。

据我们数据结构的老师说,由于数据在存储时大多是稀疏矩阵,存在许多的空间浪费,而磁盘碎片整理优化的就是这个。


清理系统文件

在前面的磁盘清理界面中我们还可以看到清理系统文件这一选项,可以选择它进行进一步清理,这里面有一项是以前的windows安装文件,如果不打算回退的话清理无妨。

有可能你还会注意到一个名为“系统错误内存转储文件”的选项,这个也可以大胆清除,对一般使用者(基本不需排查系统问题)这个文件完全没有任何作用。

对于防止系统错误内存转储文件占用空间,还有一劳永逸直接禁止生成的办法,首先打开高级系统设置,来到如图所示选项卡。

打开“启动和故障修复”设置窗口,在写入调试信息的下拉列表中选择“无”并确定即可。

当然,如果你觉得你或许用得上系统错误内存转储文件,那么也可以选择这里的小内存转储或者核心内存转储,这样同样也能节省空间。
清理系统文件是给windows10瘦身最有效的办法之一,事实上,若无特殊需求,扫描结果中的文件均可以勾选清除。


删除临时文件

这里有两个临时文件中的全部文件可以删除,一个是C:\Users\用户名\AppData\Local\Temp目录下的文件,这里是临时文件最多的地方,可以上到几个G;另一个是C:\Windows\Temp,这里文件大小相对较小,可以忽略不计。另外我也找到了一些其他的临时文件,但似乎它们的体积都是0,可能是系统自动清理了。

注意:千万不要误删上一级目录!如果担心删除出错,可先放到回收站,重启之后看有无异常再做决定。亲测上述两个文件夹中的所有文件均可删除。


删除冗余更新

众所周知,windows系统会自动更新,这也是许多人弃windows的一大原因,然而windows还是要用的,于是我找到了一种可以清理多余的windows更新文件的方法。

  1. 首先右键左下角开始菜单,选择“Windows PowerShell(管理员)”。 弹出是否允许进行更改选择“是”。
  2. 输入命令dism.exe /Online /Cleanup-Image /AnalyzeComponentStore并执行。
  3. 这时候会显示“推荐使用组件存储清理:是or否”,因为我前不久清过,所以这里显示为“否”,那么就别清理了。如果显示为“是”,那么进行第四步。
  4. 输入命令dism.exe /online /Cleanup-Image /StartComponentCleanup并执行。
    这样电脑就会开始清理啦。这个过程会比较长,不要着急和担心,在这期间你可以做些别的事情,比如看看我其他的几篇博客。

关闭休眠功能

电脑开启休眠功能后,系统自动生成内存镜像文件,以便唤醒电脑以后可以快速开启程序。不过休眠有个缺点,那就是会占用大部分的系统盘空间(约3G),这个文件在C盘的根目录,名为hiberfil.sys,若觉得这个功能不必要的话,就可以关闭休眠以节省磁盘空间。

  1. 打开“Windows PowerShell(管理员)”,方法同上。
  2. 输入powercfg -h off,回车。
    这样就成功关闭了休眠功能。此时Cpan内已没有hiberfil.sys这个文件了。

开启自动清理

打开存储设置,开启存储感知,这样系统就会自动清理了。
可以通过“配置存储感知或立即运行”选项来调整自动清理方式或者立即清理,这种方法也很强力,能释放上G的空间,推荐使用!


重装系统

俗话说得好“大力出奇迹”,重装系统无疑是最有效清理空间的一种方法。只要备份好数据,重装其实没有想象的那么困难。我本人这一年多来就重装过3次系统(两次是被迫的…),其实装了几次就熟练了,我曾看到某linux大牛(忘了是谁)总共重装了19次linux。你可以在我的另一篇博文ubuntu笔记:重装ubuntu——记一次辛酸血泪史中看到如何在双系统情况下重装ubuntu的过程。


软件安装管家

本文中的部分方法来源于公众号“软件安装管家”的推送,在这里安利一下。该公众号提供了丰富且稳定的破解软件安装(VS、AD、PS等),此外,这个公众号的客服还是一个难得的段子手!刷评论区绝对可以成为你的快乐源泉哈哈。


碰到底线咯 后面没有啦

本文标题:windows笔记:释放空间

文章作者:高深远

发布时间:2019年09月14日 - 09:10

最后更新:2020年02月25日 - 08:29

原始链接:https://gsy00517.github.io/windows20190914091023/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
windows笔记:释放空间 | 高深远的博客

windows笔记:释放空间

暑假里想跑CVPR中的代码,发现作者提供的环境配置都是基于linux终端的,这样windows的git bash就满足不了我了。二话不说我花了两天时间装了个ubuntu+windows双系统,好不容易装好了,却发现我的硬盘空间已经岌岌可危(理论上要留内存的三倍左右可以保证系统顺畅运行,我的内存是8G,也就是说我C盘应空出20G左右为宜)。于是我就找了些释放空间的办法,分享在这里。

References

电子文献:
http://www.udaxia.com/wtjd/9147.html


利用磁盘属性进行清理

这是最稳的方法,但释放的空间也相对较少,不过还是有效的。选择“此电脑”,右键C盘,属性,然后就可以在常规下面看到磁盘清理。一般按默认选择的进行清理,当然全点上勾也无所谓。

注意:千万不能选择压缩此驱动器以节约磁盘空间!

另外在工具下面你可以看到一个优化的选项,一般系统会定期自动执行优化,如果你是强迫症,时时刻刻都容不得一点冗余的话,可以手动优化。

据我们数据结构的老师说,由于数据在存储时大多是稀疏矩阵,存在许多的空间浪费,而磁盘碎片整理优化的就是这个。


清理系统文件

在前面的磁盘清理界面中我们还可以看到清理系统文件这一选项,可以选择它进行进一步清理,这里面有一项是以前的windows安装文件,如果不打算回退的话清理无妨。

有可能你还会注意到一个名为“系统错误内存转储文件”的选项,这个也可以大胆清除,对一般使用者(基本不需排查系统问题)这个文件完全没有任何作用。

对于防止系统错误内存转储文件占用空间,还有一劳永逸直接禁止生成的办法,首先打开高级系统设置,来到如图所示选项卡。

打开“启动和故障修复”设置窗口,在写入调试信息的下拉列表中选择“无”并确定即可。

当然,如果你觉得你或许用得上系统错误内存转储文件,那么也可以选择这里的小内存转储或者核心内存转储,这样同样也能节省空间。
清理系统文件是给windows10瘦身最有效的办法之一,事实上,若无特殊需求,扫描结果中的文件均可以勾选清除。


删除临时文件

这里有两个临时文件中的全部文件可以删除,一个是C:\Users\用户名\AppData\Local\Temp目录下的文件,这里是临时文件最多的地方,可以上到几个G;另一个是C:\Windows\Temp,这里文件大小相对较小,可以忽略不计。另外我也找到了一些其他的临时文件,但似乎它们的体积都是0,可能是系统自动清理了。

注意:千万不要误删上一级目录!如果担心删除出错,可先放到回收站,重启之后看有无异常再做决定。亲测上述两个文件夹中的所有文件均可删除。


删除冗余更新

众所周知,windows系统会自动更新,这也是许多人弃windows的一大原因,然而windows还是要用的,于是我找到了一种可以清理多余的windows更新文件的方法。

  1. 首先右键左下角开始菜单,选择“Windows PowerShell(管理员)”。 弹出是否允许进行更改选择“是”。
  2. 输入命令dism.exe /Online /Cleanup-Image /AnalyzeComponentStore并执行。
  3. 这时候会显示“推荐使用组件存储清理:是or否”,因为我前不久清过,所以这里显示为“否”,那么就别清理了。如果显示为“是”,那么进行第四步。
  4. 输入命令dism.exe /online /Cleanup-Image /StartComponentCleanup并执行。
    这样电脑就会开始清理啦。这个过程会比较长,不要着急和担心,在这期间你可以做些别的事情,比如看看我其他的几篇博客。

关闭休眠功能

电脑开启休眠功能后,系统自动生成内存镜像文件,以便唤醒电脑以后可以快速开启程序。不过休眠有个缺点,那就是会占用大部分的系统盘空间(约3G),这个文件在C盘的根目录,名为hiberfil.sys,若觉得这个功能不必要的话,就可以关闭休眠以节省磁盘空间。

  1. 打开“Windows PowerShell(管理员)”,方法同上。
  2. 输入powercfg -h off,回车。
    这样就成功关闭了休眠功能。此时Cpan内已没有hiberfil.sys这个文件了。

开启自动清理

打开存储设置,开启存储感知,这样系统就会自动清理了。
可以通过“配置存储感知或立即运行”选项来调整自动清理方式或者立即清理,这种方法也很强力,能释放上G的空间,推荐使用!


重装系统

俗话说得好“大力出奇迹”,重装系统无疑是最有效清理空间的一种方法。只要备份好数据,重装其实没有想象的那么困难。我本人这一年多来就重装过3次系统(两次是被迫的…),其实装了几次就熟练了,我曾看到某linux大牛(忘了是谁)总共重装了19次linux。你可以在我的另一篇博文ubuntu笔记:重装ubuntu——记一次辛酸血泪史中看到如何在双系统情况下重装ubuntu的过程。


软件安装管家

本文中的部分方法来源于公众号“软件安装管家”的推送,在这里安利一下。该公众号提供了丰富且稳定的破解软件安装(VS、AD、PS等),此外,这个公众号的客服还是一个难得的段子手!刷评论区绝对可以成为你的快乐源泉哈哈。


碰到底线咯 后面没有啦

本文标题:windows笔记:释放空间

文章作者:高深远

发布时间:2019年09月14日 - 09:10

最后更新:2020年02月25日 - 08:29

原始链接:https://gsy00517.github.io/windows20190914091023/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
windows笔记:一些快捷的操作 | 高深远的博客

windows笔记:一些快捷的操作

之前在网上看到一个windows系统下的上帝模式,很好奇,尝试之后感觉不错,这里介绍一下创建的方法。除此之外附上一些类似的快捷操作。


上帝模式

上帝模式,即”God Mode”,或称为“完全控制面板”。它是windows系统中隐藏的一个简单的文件夹窗口,包含了几乎所有windows系统的设置,如控制面板的功能、界面个性化、辅助功能选项等控制设置,用户只需通过这一个窗口就能实现所有的操控,而不必再去为调整一个小小的系统设置细想半天究竟该在什么地方去打开设置。打开上帝模式后你将会看到如下界面:

好吧我承认和想象中的上帝模式不太一样,不过下面我还是介绍一下这个略显简陋的上帝模式是怎么设置的。

  1. 方式一:添加桌面快捷方式

    1. 首先在桌面新建一个文件夹。
    2. 将新建的文件夹命名为:GodMode.{ED7BA470-8E54-465E-825C-99712043E01C}
    3. 重命名完成后,你将看到一个类似于控制面板但没有名称的图标,双击打开,就可以看到之前所展示的上帝模式的界面了。
  2. 方式二:添加到快捷菜单

    1. win+R运行,输入regedit打开注册表编辑器,允许更改。
    2. 依次展开路径至HKEY_CLASSES_ROOT\DesktopBackground\Shell
    3. 点击shell后在右侧窗口鼠标右击,选择新建项。
    4. 把新建的项重命名为“上帝模式”。
    5. 点击上帝模式后,双击右侧窗口中的默认,在数值数据处输入“上帝模式”,点击确定。
    6. 右击上帝模式,选择新建项。
    7. 把新建的项重命名为“command”。
    8. 点击command后,双击右侧窗口中的默认,在数值数据处输入:explorer shell:::{ED7BA470-8E54-465E-825C-99712043E01C},确定。
    9. 这时候在桌面空白处右键打开快捷菜单,就可以看到上帝模式已成功添加。

类似的操作

在上面我的快捷菜单中,可以看到还有关机、重启、锁屏等选项。其实它们添加的操作和添加上帝模式的步骤是一样的,只需把命名为“上帝模式”的地方修改成“关机”等文字,并且在上文中的第8步中,用对应的数值数据即可。

这里提供四种功能对应的数值数据,其实这些和上面上帝模式的commmand命令都是可以直接在cmd中执行的:
关机Shutdown -s -f -t 00
注销Shutdown -l
重启Shutdown -r -f -t 00
锁屏Rundll32 User32.dll,LockWorkStation
事实上,锁屏功能可以直接使用win+L快捷键达到目的。除此之外,win还可以搭配其他的一些按键完成一些快捷操作,比如win+D可以快速最小化一切窗口回到桌面,想知道win有哪些搭配可以右键左下角的win图标查看。


碰到底线咯 后面没有啦

本文标题:windows笔记:一些快捷的操作

文章作者:高深远

发布时间:2019年10月01日 - 13:05

最后更新:2020年02月07日 - 17:06

原始链接:https://gsy00517.github.io/windows20191001130531/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
windows笔记:一些快捷的操作 | 高深远的博客

windows笔记:一些快捷的操作

之前在网上看到一个windows系统下的上帝模式,很好奇,尝试之后感觉不错,这里介绍一下创建的方法。除此之外附上一些类似的快捷操作。


上帝模式

上帝模式,即”God Mode”,或称为“完全控制面板”。它是windows系统中隐藏的一个简单的文件夹窗口,包含了几乎所有windows系统的设置,如控制面板的功能、界面个性化、辅助功能选项等控制设置,用户只需通过这一个窗口就能实现所有的操控,而不必再去为调整一个小小的系统设置细想半天究竟该在什么地方去打开设置。打开上帝模式后你将会看到如下界面:

好吧我承认和想象中的上帝模式不太一样,不过下面我还是介绍一下这个略显简陋的上帝模式是怎么设置的。

  1. 方式一:添加桌面快捷方式

    1. 首先在桌面新建一个文件夹。
    2. 将新建的文件夹命名为:GodMode.{ED7BA470-8E54-465E-825C-99712043E01C}
    3. 重命名完成后,你将看到一个类似于控制面板但没有名称的图标,双击打开,就可以看到之前所展示的上帝模式的界面了。
  2. 方式二:添加到快捷菜单

    1. win+R运行,输入regedit打开注册表编辑器,允许更改。
    2. 依次展开路径至HKEY_CLASSES_ROOT\DesktopBackground\Shell
    3. 点击shell后在右侧窗口鼠标右击,选择新建项。
    4. 把新建的项重命名为“上帝模式”。
    5. 点击上帝模式后,双击右侧窗口中的默认,在数值数据处输入“上帝模式”,点击确定。
    6. 右击上帝模式,选择新建项。
    7. 把新建的项重命名为“command”。
    8. 点击command后,双击右侧窗口中的默认,在数值数据处输入:explorer shell:::{ED7BA470-8E54-465E-825C-99712043E01C},确定。
    9. 这时候在桌面空白处右键打开快捷菜单,就可以看到上帝模式已成功添加。

类似的操作

在上面我的快捷菜单中,可以看到还有关机、重启、锁屏等选项。其实它们添加的操作和添加上帝模式的步骤是一样的,只需把命名为“上帝模式”的地方修改成“关机”等文字,并且在上文中的第8步中,用对应的数值数据即可。

这里提供四种功能对应的数值数据,其实这些和上面上帝模式的commmand命令都是可以直接在cmd中执行的:
关机Shutdown -s -f -t 00
注销Shutdown -l
重启Shutdown -r -f -t 00
锁屏Rundll32 User32.dll,LockWorkStation
事实上,锁屏功能可以直接使用win+L快捷键达到目的。除此之外,win还可以搭配其他的一些按键完成一些快捷操作,比如win+D可以快速最小化一切窗口回到桌面,想知道win有哪些搭配可以右键左下角的win图标查看。


碰到底线咯 后面没有啦

本文标题:windows笔记:一些快捷的操作

文章作者:高深远

发布时间:2019年10月01日 - 13:05

最后更新:2020年02月07日 - 17:06

原始链接:https://gsy00517.github.io/windows20191001130531/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
windows笔记:刷新DNS解析缓存 | 高深远的博客

windows笔记:刷新DNS解析缓存

今天我购买的VPN订阅服务的服务器由于技术人员的误操作导致系统被重建,好在可靠的商家留存了备份并且在第一时间重建了。然而尽管商家那边的主域名一定DNS生效了,我本地依旧无法成功通过对应域名成功登录。于是我找到了下面这种立即刷新DNS缓存的方法。


DNS缓存

为了提高网站的访问速度,系统会在成功访问某网站后将该网站的域名、IP地址信息缓存到本地。这样在下次访问该域名时,可以直接通过IP进行访问。可是,如果一些网站的域名没有变化但IP地址发生变化(比如上述情况),就有可能因本地的DNS缓存没有刷新导致无法访问。


立即刷新方法

在windows下,win+R输入cmd打开命令行,输入ipconfig /flushdns并执行,此时就完成了对DNS缓存的立即刷新。


碰到底线咯 后面没有啦

本文标题:windows笔记:刷新DNS解析缓存

文章作者:高深远

发布时间:2020年03月12日 - 16:35

最后更新:2020年03月12日 - 16:44

原始链接:https://gsy00517.github.io/windows20200312163504/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%
windows笔记:刷新DNS解析缓存 | 高深远的博客

windows笔记:刷新DNS解析缓存

今天我购买的VPN订阅服务的服务器由于技术人员的误操作导致系统被重建,好在可靠的商家留存了备份并且在第一时间重建了。然而尽管商家那边的主域名一定DNS生效了,我本地依旧无法成功通过对应域名成功登录。于是我找到了下面这种立即刷新DNS缓存的方法。


DNS缓存

为了提高网站的访问速度,系统会在成功访问某网站后将该网站的域名、IP地址信息缓存到本地。这样在下次访问该域名时,可以直接通过IP进行访问。可是,如果一些网站的域名没有变化但IP地址发生变化(比如上述情况),就有可能因本地的DNS缓存没有刷新导致无法访问。


立即刷新方法

在windows下,win+R输入cmd打开命令行,输入ipconfig /flushdns并执行,此时就完成了对DNS缓存的立即刷新。


碰到底线咯 后面没有啦

本文标题:windows笔记:刷新DNS解析缓存

文章作者:高深远

发布时间:2020年03月12日 - 16:35

最后更新:2020年03月12日 - 16:44

原始链接:https://gsy00517.github.io/windows20200312163504/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%