记得本科的时候,老师说“一个算法如果能做到两个东西,就算比较完美了: 首先,能动态地增加类别,其次,能动态地进行训练。”iCaRL 使用一个比较暴力的方法解决了第一个问题。
下面的内容纯属个人理解,如有理解不对的地方(应该会很多),请直接在下面评论处打脸,谢谢。
一般来说,分类算法在训练之前是需要知道 “总共有多少个类” 的。一旦固定下来,就不能变了。如果要增加类,那么需要从头重新训练分类器。
或者还有一个办法:找到一个比较好的特征,然后对每个类训练一个分类器(接 N 个 0/1 分类器)。新来一个类,使用原来的特征提取方法,得到特征后加一个针对新类的分类器就好了。
iCaRL 使用的是一种类似于聚类的方法:已知 C 个类和一大堆样本,那么可以计算这 C 个中每个类的“均值(prototype)”。新来一个样本,通过一个特征提取算法提取出特征之后,找最近邻,取其类别标签即可(算法 1)。现在希望新加一个类,那么就根据已知的新类的样本,算出新类的均值(算法 2)。这样再新来一个样本,就能和上面一样进行操作了。
到这里问题就能简单暴力地解决了。但作者又考虑了一些事情:
首先,按照上面的说法,新加一个类,就需要加入一些样本,这样随着类越加越多,算法需要保存的样本也越来越多。能不能设置一个 “整个算法最多保留多少个样本” 呢?假设这个上限是 M,那么每个类最多保存 $$\frac{M}{C}$$ 个样本。新加入 A 个类之后,每个类最多保存 $$\frac{M}{C+A}$$ 个样本。所以加类之后之前的每个类里面可能需要剔除一些样本。选择什么样的样本进行剔除呢?当然是剔除那些 “不怎么好” 的样本,也就是离聚 “类均值” 比较远的样本(算法 4、5)。剔除样本后,原来类的均值就变了,需要重新计算一下。
其次,新加一个类,之前的特征提取算法也不能完全适用了,所以需要更新一下。这里使用 CNN 进行特征提取,作者把更新特征提取器的过程当成了一个训练 CNN 的过程,利用 CNN 训练的反向传播过程更新特征提取器(算法 3)。(这里的 Loss 函数没太看懂)
然后,由于特征提取器是一直在变的,这就导致每张图的特征也一直在变,所以中间过程中不能存特征,而应该存原图。哦原图太多,内存占满?那就存原图的路径,每次用到图片的时候就从磁盘读取就好了。
上面说了文章,下面开始说代码。作者提供了 Theano 和 TensorFlow 的代码,可谓良心。
首先需要解决数据读取的问题。作者说“所有图片都需要放在同一个文件夹下,如果不是这个格式的话,需要自己改代码去”。自己的数据是一个检测数据集,图片文件分散在 NNN 多个文件夹中,有类似于 VOC 的标注和自己转的 YOLO 的标注。根据标注生成了个 图片绝对路径 类别
TXT 文件,后来发现需要改太多的代码,一天多之后放弃了。用脚本读取这个 TXT,将图片重命名并移动到同一个文件夹下。世界安静了。
之后要解决一些 BUG,例如 Python2 和 Python3 的兼容性问题(cPickle
在 Python3 下叫做 _pickle
),作者的 Typo(cPickle.dump
的时候,文件没有以二进制的方式打开、cPickle 保存文件的版本等)。作者使用的 TensorFlow 版本较老,没有用 DatasetAPI,输入队列需要自己控制,在启动了线程之后没有关闭,会导致一堆 Warning 并且最后程序推出的时候返回值为 -1
,我们还需要补上几句话来结束输入循环。
最后要解决的一个 BUG 是类型不匹配。运行代码的时候一直报 os.path.join()
不能拼接字符串和二进制字符串。调试了一整天最后找到真凶:程序从 TensorFlow 的 sess.run()
中获取到了一个 “已经处理过” 的文件列表 processed_files
,并加入到 files_from_cl
数组中。这个数组前一部分(超过 300 项)都是 string,但是从 TensorFlow 中获取到的 processed_files
是二进制字符串,加在了 files_from_cl
数组最后面。PyCharm 默认只能看到数组的前 300 项,新加进来的二进制字符串默认看不到,也想不到会是这样的错误。发现这个问题后,在相应位置补写一句 processed_files=np.array([x.decode() for x in processed_files])
就好了。
最后 * 猜 * 一下代码的意思。代码中最前面有三个变量 nb_cl
、nb_group
和 nb_proto
,看注释没看懂。但是猜测是这样的:程序要运行 nb_group
次,每次添加 nb_cl
个类,限制每个类里面最多有 nb_proto
个样本。每次训练,程序会随机分配所有的类(也就是,不确定每次新增哪几个类。所以我是总共 24 个类分成了 3 组,每组正好 8 个类),并自己分出来训练集和测试集,并保证训练时只用训练集、测试时只用测试集。程序使用 ResNet18 作为特征提取器,但并没有使用 ImageNet 预训练权重(表示非常不理解)。测试代码能直接跑,解决完 BUG 之后就没有仔细读。
如果上述设定(初始 8 类,每次加 8 类,加 2 次)和理解没有错的话,在自己的数据集上训练 50 个 epoch,分别得到了大约 “8类97、16类98、24类94.5” 的 Top1 Accuracy,Top5 Accuracy 好像都是 1。可能是自己的数据集太小(每类只有 150 张左右图片),并且是从视频中截取出来的图片,类内太过相似造成的吧。
发表回复