机器学习代码心得之迭代器和流水处理

作者:陈天奇 ,本文首发在作者的个人微博 。

很多机器学习程序涉及从外存的数据读取以及预处理。常见的例子比如深度的神经网络,或者是基于外存计算的一些算法如VW还有我很早之前写过的SVDFeature。在这类问题中,一个常见的优化是采用一个单独的线程来进行数据的预读或者预处理,而用另外一个线程进行计算。

现在有这样一个问题:假设我们可以从硬盘文本格式,jpg以及各种格式来进行读入,而读入的实现多种多样,并且包含一些可能的预处理。但是我们自己又不想每次写各种多线程代码,怎么样才可以抽象出一个通用的模块解决这一个问题。

这里提供了一个解决方案,迭代器。迭代器(iterator)是设计模式中最常见的模式之一,它特别适用于在线更新的机器学习算法。一般常见的迭代器的接口如下

DataIterator {

void Reset();

bool Next();

DataBatch GetBatch();

};

我们通过Next移动迭代器,GetBatch拿到当前的数据块,Reset恢复迭代器到数据起始位置。一般一个常见的在线更新算法可以拿到一个迭代器,并且做如下操作

while (iter.Next()) {

Update(iter.GetBatch());
}

对于不同的数据,我们只需要实现各个格式的迭代器即可。这样对于机器学习的部分不需要关心数据的读入逻辑,就可以进行统一的计算。迭代器的抽象一大优势是可嵌套,一个迭代器的实现可以以另外一个迭代器作为输入。比如我们在读入图片的时候要对图片进行随机旋转,那我们可以设计一个随机旋转迭代器,每一次Next的时候从输入迭代器拿到一个图片,进行随机旋转,返回给使用者。大概的代码如下:

RandomProcIter : publicDataIterator {

DataIterator base;

DataBatch out;

bool Next() {

if (!base.Next()) return false;

out = rotate(iter.GetBatch());

}

DataBatch GetData() {

return out;

}

}

这样的好处是我们可以把预处理代码和读入代码分开,对于不同格式的数据读入我们都可以嵌一个处理迭代器来进行处理。但是现在的问题是,如何支持采用一个独立的线程来做预读。答案很简单,设计一个线程缓冲迭代器(thread buffer iterator)。

想法如下,设计一个迭代器以其它任意迭代器作为输入,在这个迭代器内部开启一个独立的线程,来主动地把输入迭代器的数据读入到内部的一个缓冲里面去。在caller调用Next和GetData的时候,只要直接返回缓冲里面已有的结果就好了。

这样设计的好处是所有的读入接口只是迭代器,并没有多线程的痕迹,而我们也只需要实现一次线程缓冲迭代器的代码。整个读入像是一个迭代器的chain,我们可以任意地引入线程缓冲迭代器让一个单独的线程来做前面这部分数据的预读,比如一个可能的迭代器链可以是:

JPegIterator->ThreadBuffer->ResizeRotationCrop->ThreadBuffer

这样的话有一个独立的线程去预读取jpeg图片,采用另外一个线程来进行图片的旋转等预处理。比起专门写多线程代码来的更加灵活地多。这类处理方式一般称为流水线(pipeline)

值得一提的是这类思想来源于数据库领域,在一般数据库的query execution就是用迭代器的嵌套组合来实现的。

留下你的评论