大数据仁波切的live笔记

毫无疑问,大数据仁波切刘鹏的知乎live《如何成为数据科学家?》是一场好live。支撑上述结论的主要有两点:

  • 价格便宜,开始前购买只要两块三毛四。
  • 不装逼

而青年人也有两个显著特点,一是没钱,二是容易被忽悠,所以,这是一场好live,除了广告有些多。我也不打算记一篇完整的笔记,只挑一些自己觉得有意思的点分享一下。

关于大数据的概念

大数据这个概念既不是工业界也不是学术界提出的,而是咨询公司提出来并炒热的。虽然谷歌是大数据方面的一哥与PR高手,但并没有像炒作AI一样炒作大数据这个概念,美帝的学术界对此更是不感冒。

关于大数据的定义也是众说纷纭,有人认为数据量大就是大数据,还是有人认为大数据有4V的特点,实际上无论是数据量大还是4V标准,都是非常模糊的标准,不能用来衡量一个具体是否是大数据问题。

仁波切给出了数据科学家视角下大数据概念的定义:

大数据的本质

  • 使用行为数据。行为数据是区别于交易数据的一个概念。交易数据即
    业务过程中必须记录的数据,例如电信运营商纪录的用户的通话纪录、充值记录、扣费日志等。行为数据并不是必须纪录的数据,比如用户的地理位置、浏览记录等。

  • 全量加工。大数据问题一般无法通过传统的少量抽样的方式来解决,需要使用全量数据。

  • 自动化应用。自动化应用是相对于洞察应用而言的。所谓洞察应用即将数据可视化成人可理解的报表等形式,为后续的运营决策提供参考。自动化应用是数据->机器的过程,数据自动决策。计算广告、个人征信都是典型的自动化应用。

数据科学家的定义与核心竞争力

工业界传统的数据挖掘是非常强调领域知识的,甚至推崇规则甚于算法。而仁波切定义的数据科学家区别于传统的数据挖掘工程师——

“数据科学家是指采用科学的方法论,调动充足的计算能力,将大量人类无法直接处理的数据转化为有用信息,以驱动自动化业务决策的专家”。

另外,需要补充一点,“科学家”在这里只是从事数据或算法相关工作的工程师的别称,请千万不要误会。定义中强调了“科学的方法论”,有意忽略了经验,数据科学家虽然不是真正的科学家,但也要有摆脱各种tricks的泥潭的追求。

一个数据科学家,当然要懂统计、最优化、分布式计算、常见机器学习算法的原理与应用,还要有领域知识,但除了领域知识以外其他技能点都是没有门槛的,智力正常的人看看书、做做练习都能够掌握,在相关领域进行实践之后,领域知识获取也没有太大难度。

真正能够区分普通数据工作者与优秀数据工作者的核心竞争力是建模能力,更通俗地说是定义损失函数的能力。选择已有的机器学习算法应用到新问题可以做优秀,但无法做到最顶尖,“高玩都是自定义配置的”。

数据科学家的养成途径

仁波切用三层金字塔表示了数据科学家的养成途径,从下往上分别是技能、能力和意识。

  • 技能。基础中的基础,自学或跟课程都可以完成。
  • 能力。需要在实践中培养,抓住一个问题,无论是广告还是推荐或者征信,做熟做透,有意培养建模能力,一样熟了,培养了感性认识,其他的也不难。
  • 意识。技能需要刻意练习,意识当然也要特意培养。数据优于经验、计算优于人工无非就是强调数据科学家要相信数据、相信机器,安身立命的根基怎么能不相信呢?

自学的资料推荐

仁波切强调了学习资料不在多,而是将优秀的资料啃透。仁波切博士毕业,毕业后也泡过学术界,推荐的资料门槛较高,可能不是适合所有人。但是,建议是很对的,读透一本书胜过读一百本书的序言。

Caffe中的Net类是如何工作的

Net类是Caffe中Blobs,Layers,Nets三个抽象层次中最高层的抽象。Nets类负责按照网络定义文件将需要的layers和中间blobs进行实例化,并将所有的Layers组合成一个有向无环图。Nets还提供了在整个网络上进行前向传播与后向传播的接口。下面从观察Net运行的角度来解析一下Net类如何工作。

Net类数据成员概述

下面对Net类中比较重要的数据成员进行说明:

  • vector<shared_ptr<Layer<Dtype> > > layers_;
    layers_中存放着网络的所有layers,也就是Net类的实例保存着网络定义文件中所有layer的实例

  • vector<shared_ptr<Blob<Dtype> > > blobs_;
    blobs_中保存着网络所有的中间结果,即所有layer的输入数据(bottom blob)和输出数据(top blob)

  • vector<vector<Blob<Dtype>*> > bottom_vecs_;

  • vector<vector<Blob<Dtype>*> > top_vecs_;
    bottom_vecs_保存的是各个layer的bottom blob的指针,这些指针指向blobs_中的blob。bottom_ves.size()与网络layer的数量相等,由于layer可能有多个bottom blob,所以使用vector<Blob<Dtype>*>来存放layer-wise的bottom blob。同理可以知道top_vecs的作用。

  • vector<shared_ptr<Blob<Dtype> > > params_;

  • vector<Blob<Dtype>*> learnable_params_;
    上述两个数据成员存放的是指向网络参数的指针,注意,直接拥有参数的是layer,params_保存的只是网络中各个layer的参数的指针;而learnable_params_也如其名字所指,保存的是各个layer中可以被学习的参数。

Net类的实例化(一个网络的建立)

构造函数

Net类有两个构造函数,分别是Net(const NetParameter& param, const Net* root_net)Net(const string& param_file, Phase phase, const Net* root_net),前者接受NetParameter的const引用作为参数(后面参数root_net与多GPU并行训练有关,忽略掉并不影响理解),后者接受定义网络prototxt文件路径和phase作为输入。
前者直接调用Init()函数,后者将prototxt文件解析为NetPrameter后调用Init()函数。

Init()函数

Init()函数承担初始化一个网络的任务,摘取主干代码描述如下(忽略细节,大致描述过程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (int layer_id = 0; layer_id < param.layer_size(); ++layer_id) {//param是网络参数,layer_size()返回网络拥有的层数
const LayerParameter& layer_param = param.layer(layer_id);//获取当前layer的参数
layers_.push_back(LayerRegistry<Dtype>::CreateLayer(layer_param));//根据参数实例化layer


//下面的两个for循环将此layer的bottom blob的指针和top blob的指针放入bottom_vecs_和top_vecs_,bottom blob和top blob的实例全都存放在blobs_中。相邻的两层,前一层的top blob是后一层的bottom blob,所以blobs_的同一个blob既可能是bottom blob,也可能使top blob。
for (int bottom_id = 0; bottom_id < layer_param.bottom_size();++bottom_id) {
const int blob_id=AppendBottom(param,layer_id,bottom_id,&available_blobs,&blob_name_to_idx);
}

for (int top_id = 0; top_id < num_top; ++top_id) {
AppendTop(param, layer_id, top_id, &available_blobs, &blob_name_to_idx);
}

//接下来的工作是将每层的parameter的指针塞进params_,尤其是learnable_params_。
const int num_param_blobs = layers_[layer_id]->blobs().size();
for (int param_id = 0; param_id < num_param_blobs; ++param_id) {
AppendParam(param, layer_id, param_id);
//AppendParam负责具体的dirtywork
}

}

初始化之后

经过上述过程的网络,参数都是随机产生或者指定的,如果进行预测或这fine-tuning,就需要将载入预训练的权值,Net类提供的函数CopyTrainedLayersFrom(const string& trained_file)可以实现这个过程。

网络的运行(前向传播, 反向传播和权值更新)

Net类可以提供网络级的前向前向传播、反向传播和权值更新(即在网络的所有层上有序执行前述动作)。

前向传播

与前向传播相关的函数有Forward(const vector<Blob<Dtype>*> & bottom, Dtype* loss),Forward(Dtype* loss),ForwardTo(int end)ForwardFrom(int start)ForwardFromTo(int start, int end),前面的四个函数都是对第五个函数封装,第五个函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
    template <typename Dtype>
Dtype Net<Dtype>::ForwardFromTo(int start, int end) {
CHECK_GE(start, 0);
CHECK_LT(end, layers_.size());
Dtype loss = 0;
for (int i = start; i <= end; ++i) {
// LOG(ERROR) << "Forwarding " << layer_names_[i];
Dtype layer_loss = layers_[i]->Forward(bottom_vecs_[i], top_vecs_[i]);
loss += layer_loss;
if (debug_info_) { ForwardDebugInfo(i); }
}
return loss;
}

重点语句是`layers_[i]->Forward(bottom_vecs_[i], top_vecs_[i]);`,使用layer对应bottom blob和top blob进行前向传播。

反向传播

与前向传播一样,反向传播也有很多相关函数,但都是对BackwardFromTo(int start, int end)的封装。

Net::BackwardFromTo(int start, int end) {
1
  CHECK_GE(end, 0);
  CHECK_LT(start, layers_.size());
  for (int i = start; i >= end; --i) {
    if (layer_need_backward_[i]) {
      layers_[i]->Backward(top_vecs_[i], bottom_need_backward_[i], bottom_vecs_[i]);
      if (debug_info_) { BackwardDebugInfo(i); }
    }
  }
}

与前向传播相反,反向传播是从尾到头进行的。

权值更新

1
2
3
4
5
6
template <typename Dtype>
void Net<Dtype>::Update() {
for (int i = 0; i < learnable_params_.size(); ++i) {
learnable_params_[i]->Update();
}
}

在训练的过程中layer的权值要根据反向传播并累积的梯度进行更新,更新的过程由Update()完成。这个函数的功能十分明确,对每个存储learnable_parms的blob调用blob的Update()函数,来更新权值。

在Windows中阅读Caffe代码

前言

我们都知道世界上最好的IDE是Visual Studio,如果能用VS来调试、阅读Caffe的代码,观察Caffe运行过程,相信理解的效率会大大提高,但将Caffe移植到Windows上非常耗时耗力。
最近微软官方发布了Caffe的Windows branch,使用Nuget自动下载配置Caffe各种依赖,使得Caffe在Windows下的安装运行比在原生的Linux环境下更加简单。本文勾勒一下从安装到调试运行的大致步骤,希望能对后来者有帮助。

Caffe的安装

  • 安装Visual Studio 2013
  • 下载Windows branch的git文件,并按照README.md中的指导进行安装(如果不使用CUDA,记得按照指导修改“CommonSettings.props”文件)。

    调试与运行

  • 第一次进行编译时Nuget会自动下载大约1G大小的依赖,请耐心等待,如果编译过程中出现“错误 8559 error C2220: 警告被视为错误…”,请参考fisherman的解决方法。
  • 首次编译成功后,鼠标右击classfication项目,之后单击”Set as StartUp Project”选项,设置程序的启动项目
    修改启动项目
  • 接着进入classfication项目的classfication.cpp文件中,拉到最下面,找到main函数,修改代码,在代码中指定模型、图片等文件的路径。然后设置断点,就可以可使用VS强大的调试功能一步步观察Caffe的运行过程了。

修改main函数

深度学习实用策略

本文翻译自DeepLearning balabala

引言

要想在实际生产中应用深度学习技术,仅仅知道算法的公式与原理是远远不够的。优秀的数据工作者需要能够根据特定的应用选择合适的算法,并能够根据实验过程中算法的反馈信息不断进行优化——是收集更多的数据,还是增加模型的复杂度,或者改变数据预处理方法,抑或是对程序进行调试等等。所有这些优化方法都需要耗费大量时间,没有方向性地乱撞显然是行不通的,需要有一些通用的、原则性的指导。

机器学习领域有各式各样的模型、训练算法与目标函数,这会给人一种“错觉”,对机器学习专家来说最重要的是掌握各式各样的机器学习技法和各个相关领域的数学知识。实际上,如果使用恰当,即使是烂大街的普通算法,也要比马马虎虎、一知半解使用的“高级”算法效果要好很多。而正确地使用算法很简单,掌握一些很简单的原则与策略就足够了。下面是推荐的机器学习系统设计流程:

  • 决定目标——首先要明确算法的性能衡量指标(原文是error metric),以及要将这个衡量指标优化到什么程度。
  • 尽快建立end to end的pipeline,包括恰当地估计性能指标。
  • 找出系统的瓶颈,并有针对性优化。找出系统的哪个部分表现低于预期并分析原因——是因为过拟合还是因为欠拟合,还是数据或或代码出了问题。
  • 不断地进行增量式调整——比如收集新的数据或者调整先验参数,甚至更换算法。

为了详细介绍上述的流程,将使用经典的街景门牌号识别系统为例进行说明。街景车能拍下建筑的门牌号并记录拍摄时的位置,该系统识别图片中的门牌号,并在谷歌地图中的相应位置更新信息。相信随着读者一步步了解这个成功的商业应用系统是如何构建的,可以加深对上面提到的设计流程的理解。

性能衡量指标

决定系统的性能衡量指标(即error metric)是相当重要的第一步,因为后续所有的工作都是围绕着这个指标进行的。另外,也要对系统的性能要达到何种程度做到心中有数。

系统的性能衡量指标跟算法要优化的损失函数多数情况下并不相同,比如简单二分类问题,损失函数可以是LR中交叉熵损失、后者SVM中使用hinge loss,或者是简单的感知机使用的0-1损失,但是我们对算法的性能衡量指标却可能是查全率、查准率或者是综合考虑查全率与查准率的…..。

需要明确的是,绝大多数系统都是做不到没有任何错误的。即使你有无限的多的训练数据而且能恢复真实的概率分布,也突破不了贝叶斯错误(Bayes error)的下界。因为特征含有的信息可能相对于输出变量来说并不完全(比如只知道),或者系统本身就含有随机性。何况获取无限的训练数据也是不可能的。

训练数据的多少受到多种因素的制约。实际的应用存在训练数据量与性能的trade-off,收集数据是需要付出代价的,时间、金钱、人力都需要考虑,需要在数据量增加带来的受益与收集数据付出的成本之间进行权衡。如果目的是偏学术性的,要衡量不同算法的性能,那么使用的训练与测试数据是公认的benchmark,是不能够对数据随便进行更改的。

如何决定要将衡量指标优化到什么程度呢?如果是学术研究的话,至少要比当前的其他算法性能要好后者差不多吧,否则也没有意义。实际商业应用的话,考虑的因素就多一些,

C++中的虚

C++中主要有虚函数,纯虚函数和虚继承三种“虚”。之前的一篇文章中我们讲过虚函数的实现原理。下面继续讲一讲纯虚函数和虚继承。

纯虚函数

而对于纯虚函数与虚函数的区别,可以概括为————虚函数可以让派生类选择单纯继承接口还是同时继承接口与实现,而纯虚函数强制规定派生类只继承接口并自己实现函数。

  • 定义了纯虚函数的类称为抽象类,抽象类不能够被实例化

    1
    2
    3
    4
    class abstractClass{
    public:
    virtual void pure_virtual()=0; //通用的纯虚函数声明方式
    };
  • 纯虚函数一般只给出声明而不给出定义,如果要给出定义的话,形式如下:

    1
    2
    3
    void abstractClass::pure_virtual(){
    std::cout<<"This is abstractClass::pure_virtual !"
    }
  • 只有给出纯虚函数实现的派生类才能够被实例化,否则派生类仍然是抽象类

    1
    2
    3
    4
    5
    6
    class child: public abstractClass{
    virtual void pure_virtual();
    }
    void child::pure_virtual(){
    abstractClass::pure_virtual();//这里只是距离如果纯虚函数给出了实现的话如何使用
    }

虚继承

  • 在普通继承中,派生类对象来将从基类对象继承来的非静态成员变量与自身的非静态成员变量放在同一块连续的内存中
    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
    class Parent{//打印出基类对象中非静态成员变量与派生类对象中非静态成员变量在内存中的offset
    //可以帮助我们进行理解
    public:
    int a;
    int b;

    virtual void foo(){
    cout << "parent" << endl;
    }
    };

    class Child : public Parent{
    public:
    int c;
    int d;

    virtual void foo(){
    cout << "child" << endl;
    }
    };

    int main(){

    Parent p;
    Child c;

    p.foo();
    c.foo();

    cout << "Parent Offset a = " << (size_t)&p.a - (size_t)&p << endl;
    cout << "Parent Offset b = " << (size_t)&p.b - (size_t)&p << endl;

    cout << "Child Offset a = " << (size_t)&c.a - (size_t)&c << endl;
    cout << "Child Offset b = " << (size_t)&c.b - (size_t)&c << endl;
    cout << "Child Offset c = " << (size_t)&c.c - (size_t)&c << endl;
    cout << "Child Offset d = " << (size_t)&c.d - (size_t)&c << endl;

    system("pause");
    }

最终输出结果是:

1
2
3
4
5
6
7
8
parent
child
Parent Offset a = 8
Parent Offset b = 12
Child Offset a = 8
Child Offset b = 12
Child Offset c = 16
Child Offset d = 20

  • 在多重继承中,如果继续使用单继承中的内存布局方法,可以会引起多种问题,需要另外进行设计。

考虑如下的继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class B{
public:
void foo(){
cout<<"B"<<endl;
}

};

class C:public B{
};

class D:public B{
};

class E:public C,public D{

};

int main(){
E e;
e.foo();//将会报错,比如在qt creator中编译器将抛出
//error: request for member 'foo' is ambiguous e.foo()
}

上面的代码中,派生类E有两个基类C和D,而C和D都有一个共同的基类B。这样e中就有两个B的对象,那么自然
在调用e.foo()的时候会产生歧义。另外,由于重复保存了B的对象,还会造成内存浪费。

为了防止上面的现象,引入了虚继承————继承基类时使用virtual关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class B{
public:
void foo(){
cout<<"B"<<endl;
}

};

class C:virtual public B{
};

class D:virtual public B{
};

class E:public C,public D{

};

“虚”可以认为意味着“在运行时决定”,使用虚继承的派生类在运行时才决定绑定哪个基类。C++并没有具体规定虚继承的
对象的内存布局,而是由编译器决定如何实现。一般情况下,会在虚继承的派生类对象中增添一个虚基类指针,指向基类对象。比如上面代码中E继承C和D时,会继承C和D的虚基类指针,而C和D的虚基类指针会指向同一个基类B。这样,就实现了E对象中只有一个B对象。

参考资料:

wikipedia-虚函数
memory layout of inherited class

在Caffe中使用Python Layer

Caffe通过Boost中的Boost.Python模块来支持使用Python定义Layer:

  • 使用C++增加新的Layer繁琐耗时而且很容易出错
  • 开发速度执行速度之间的trade-off

编译支持Python Layer的Caffe

如果是首次编译,修改Caffe根目录下的Makefile.cinfig,uncomment

1
WITH_PYTHON_LAYER:=1

如果已经编译过

1
2
make clean
WITH_PYTHON_LAYER=1 make&& make pycaffe

使用Python Layer

在网络的prototxt文件中添加一个Python定义的loss层如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
layer{
type: ’Python'
name: 'loss'
top: 'loss'
bottom: ‘ipx’
bottom: 'ipy'
python_param{
#module的名字,通常是定义Layer的.py文件的文件名,需要在$PYTHONPATH
module: 'pyloss'
#layer的名字---module中的类名
layer: 'EuclideanLossLayer'
}
loss_weight: 1
}

定义Python Layer

根据上面的要求,我们在$PYTHONPAT在创建pyloss.py,并在其中定义EuclideanLossLayer。

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
import caffe
import numpy as np
class EuclideadLossLayer(caffe.Layer):#EuclideadLossLayer没有权值,反向传播过程中不需要进行权值的更新。如果需要定义需要更新自身权值的层,最好还是使用C++
def setup(self,bottom,top):
#在网络运行之前根据相关参数参数进行layer的初始化
if len(bottom) !=2:
raise exception("Need two inputs to compute distance")
def reshape(self,bottom,top):
#在forward之前调用,根据bottom blob的尺寸调整中间变量和top blob的尺寸
if bottom[0].count !=bottom[1].count:
raise exception("Inputs must have the same dimension.")
self.diff=np.zeros_like(bottom[0].date,dtype=np.float32)
top[0].reshape(1)
def forward(self,bottom,top):
#网络的前向传播
self.diff[...]=bottom[0].data-bottom[1].data
top[0].data[...]=np.sum(self.diff**2)/bottom[0].num/2.
def backward(self,top,propagate_down,bootm):
#网络的前向传播
for i in range(2):
if not propagate_down[i]:
continue
if i==0:
sign=1
else:
sign=-1
bottom[i].diff[...]=sign*self.diff/bottom[i].num

原理浅析

阅读caffe源码pythonlayer.hpp可以知道,类PythonLayer继承自Layer,并且新增私有变量boost::python::object self来表示我们自己定义的python layer的内存对象。

类PythonLayer类的成员函数LayerSetUP, Reshape, Forward_cpu和Backward_cpu分别是对我们自己定义的python layer中成员函数setup, reshape, forward和backward的封装调用。

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
class PythonLayer : public Layer<Dtype> {
public:
PythonLayer(PyObject* self, const LayerParameter& param)
: Layer<Dtype>(param), self_(bp::handle<>(bp::borrowed(self))) { }

virtual void LayerSetUp(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top)
{

// Disallow PythonLayer in MultiGPU training stage, due to GIL issues
// Details: https://github.com/BVLC/caffe/issues/2936
if (this->phase_ == TRAIN && Caffe::solver_count() > 1
&& !ShareInParallel()) {
LOG(FATAL) << "PythonLayer is not implemented in Multi-GPU training";
}
self_.attr("param_str") = bp::str(
this->layer_param_.python_param().param_str());
self_.attr("setup")(bottom, top);
}
virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top)
{

self_.attr("reshape")(bottom, top);
}

virtual inline bool ShareInParallel() const {
return this->layer_param_.python_param().share_in_parallel();
}

virtual inline const char* type() const { return "Python"; }

protected:
virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top)
{

self_.attr("forward")(bottom, top);
}
virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom)
{

self_.attr("backward")(top, propagate_down, bottom);
}

private:
bp::object self_;
};

进一步了解使用C++创建新layer
参考资料

C++中的虚函数实现原理

参考资料:C++ Under the hood

虚函数是C++中多态特性的实现基础。

0.首先定义一个基类B与派生类D来辅助说明问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   class B{
public:
B();
virtual ~ B();
virtual void fun1();
virtual void fun2();
virtual void fun3();
void fun4 const();
}

class D:public B{
D();
~D();
virtual void fun1();
void fun5();

}

1.虚函数的原理可以概括如下:

  • 对于含有虚函数的类,编译器会为其建立一个虚函数表(vtbl),表中的元素是指向虚函数代码所在位置的指针。
    B的虚函数表
  • 派生类的虚函数表继承自基类并添加自己的虚函数(示例代码中D新定义的虚函数D::fun5将会被添加到虚函数表)。如果基类的虚函数被派生类重载(在上面的示例代码中基类的虚函数fun1被派生类的虚函数fun1覆盖),那么由派生类虚函数代替基类虚函数的位置(B::~B和B::fun1将被D::~D和D::fun1代替)。
    D的虚函数表
  • 含有虚函数的类的每个对象中都有一个指针(称为vptr,一般放在对象所在内存的首地址),指向类的虚函数表。
    vptr与指向的vtbl
  • 由于派生对其继承自基类的虚函数表进行了改写,所以当基类的指针或引用被绑定到派生类对象时,尽管静态类型是基类,却可以调用派生类覆盖后的虚函数。

2.多重继承时的情况

1
2
3
4
5
6
7
8
9
10
11
12
class B1{
B1()
virtual ~B();
}
class B2{
B2()
virtual ~B2();
}

class D:public B1, public B2{
~D();
}
  • 编译器会为继承自多个基类的派生类产生与基类个数相同的虚函数表,并在继承自第一个基类的虚函数表上添加派生类新定义的虚函数。
  • 派生类的对象中也会含有与基类个数相同的vptr。
  • 将派生类对象绑定到不同的基类指针(或引用)时,编译器将根据基类指针(或引用)的不同选择使用不同的虚函数表(派生类的指针(或引用)与第一个基类的指针(或引用)使用同一个虚函数表)。!
    多重继承下的虚函数表

Python在Windows环境下处理文件路径问题最佳实践

Windows中路径分隔符是反斜线’\’,而在Python中’\’又有转义符的作用,因而直接从windows资源管理器复制的路径在Python中是不能正常识别的。

  • 最优实践:
    使用os.path.join来join不同的路径,比如
    1
    path=os.path.join(dirpath,filepath)

也可以使用os.sep,python会根据不同的系统自动选择合适的路径分隔符

1
path=dirpath+os.sep+filepath

  • 次优方案
    可以将所有路径都使用正斜线’/‘,不管在Windows和Linux中都适用.
  • 不要使用
    在引用的字符串前面加上’r’可以将转义字符串(escaped strings )转换为原始字符串(raw strings),可以解决部分问题。比如:
    1
    2
    file=open('c:\myfile') #打开错误
    file=open(r'c:\myfile')#可以正确打开

但是’r’是为了方便书写正则表达式而不是为了解决Windows下文件路径问题而设计的特性,所以会遇到一下问题

file=open(r’c:\dir\’+’myfile’)

上述代码是错误的,虽然’\’失去了转义作用,但仍然有保护作用,其后的‘’’并不会被视为closing delimiter。

由此可见,使用r不仅没有完全解决问题,还会引入新的问题,所以最好不要使用。

数据挖掘中的数据预处理

0.0 关于 《DATA MINING—Concepts and Techniques》

《DATA MINING—Concepts and Techniques》是经典的数据挖掘入门书籍,内容囊括数据挖掘的基本概念、数据的预处理、数据的存储、数据中模式的挖掘、分类、聚类、异常检测等方面,作者是著名的韩家炜教授。数据的预处理在真实世界数据中是非常关键的一步,它既是不同数据挖掘应用的共同起点,又很大程度上影响了数据挖掘应用的效果。我将翻译、整理这本书中关于数据预处理的部分,如果有纰漏欢迎指正。

0.1 数据预处理综述

  • 由于真实世界中的数据来源复杂、体积巨大,往往难以避免地存在缺失、噪声、不一致(inconsistencies)等问题。
  • 当数据的维数过高时还会存在所谓的“维数诅咒(Curse of dimensionality)”问题,过高的维度不仅增加了计算量,反而可能会降低算法的效果。
  • 有些算法对数据存在特殊的要求,比如KNN、Neural Networks、Clustering等基于距离(distance based)的算法在数据进行normalize之后效果会提升。

解决上述问题需要在将数据送入算法之前进行预处理,具体包括Data Cleaning,Data IntergationData reduction,Data Transformation and Data Discretization等步骤。下面将对各个部分详细展开。

1. 数据清洗(Data Cleaning)

数据清洗的主要作用是处理数据的某些纪录值缺失,平滑数据中的噪声、发现异常值,改正不一致。

值缺失

针对数据中某些记录的值缺失问题(比如用户销售数据中,有些顾客的收入信息缺失,有些顾客的年龄信息缺失),可以采用如下的方式:

  • 忽略存在缺失值的记录。在分类问题中,如果样本(本文中,’一个样本’和’一条记录’是同义词)的label缺失了,那么这个样本一定是要丢弃的。其他情况下,除非一条记录缺失了多个值,否则这种简单粗暴的方法往往效果不好,尤其当不同的属性值缺失状况相差很大时,效果会更差。
  • 手动填写缺失的值。根据经验手工补上缺失的值,但是这种活儿谁愿意干呢?
  • 用全局的常量来替代缺失值。再拿用户销售数据做例子,对于年龄缺失的纪录年龄全用unknow来代替,但是对于数据挖掘算法来说,young和unknow并没有什么本质的不同,都只是属性值的一种而已,所以所有年龄缺失的纪录在算法看来都是同一种年龄—-“unknow”,这反而可能会降低算法的效果。
  • 使用中值和均值来替代缺失值。某种属性的中值和均值代表了此属性的平均趋势,用它们来代替缺失值不失为一种可行的方案。但是,当属性是“红、绿、蓝”这种离散值时显然不存在中值、均值的概念。
  • 使用class-specific的中值和均值来代替缺失值。在有监督问题中,一个基本假设是label相同的样本属性也相似,那么,某个样本的缺失值,用其所在类别内的所有样本的在此属性的均值或中值来代替理应效果更好。
  • 使用最可能的值来填充缺失值。用此样本的其他属性来推断缺失属性的最可能值,实际上这就变成了一个回归或分类问题(属性值连续时是回归问题,离散时是分类问题)。实际中常用贝叶斯推理或决策树来解决上述问题。

上述的第3-第6种方法都会引入偏差,因为补充的缺失值跟真实值很可能不同。第六种方法在现实中非常流行,因为它在推断缺失值时使用的信息最多,那么结果理应更准确。不过需要注意的是,有时缺失值也会提供有用的信息,比如在信用卡申请用户数据中,没有驾照号码很可能是因为没有汽车,而是否有汽车是评价信用等级很有用的信息。

噪声(noise)

噪声是混在观测值的错误(error)或误差(variance),具体去噪方式有以下几种:

  • Binning。Data Bininig,又称为Bucketing,从字面意思来展开,就是把样本点按照一定的准则分配到不同的bin(bucket)中去,然后对每个样本点根据其所在bin内样本点的分布来赋一个新值,同一个bin的样本点被赋予的新值是一致的。对于一维数据,bin可以按照区间大小划分,也可以按照data frequency来划分,而每个bin的值可以选择分布在其中样本的均值、中值或者边界值。另外,CNN中的max-pooling层,也属于data binning的范畴,典型的max-pooling层bin的尺寸为2*2,选择每个bin中的最大值作为bin四个值的新值。
  • 回归。如果变量之间存在依赖关系,即y=f(x),那么我们可以设法求出依赖关系f,从而根据x来预测y,这也是回归问题的实质。实际中更常见的假设是P(y)=N(f(x)),N是正态分布。假设y是观测值且存在噪声,如果我们能求出x和y之间的依赖关系,从而根据x来更新y的值,就可以去除其中的随机噪声,这就是回归去噪的原理。
  • 异常值检测。数据中的噪声可能有两种,一种是随机误差,另外一种可能是错误,比如我们手上有一份顾客的身高数据,其中某一位顾客的身高纪录是20m,很明显,这是一个错误,如果这个一场的样本进入了我们训练数据可能会对结果产生很大影响,这也是去噪中使用异常值检测的意义所在。当然,异常值检测远不止去噪这么一个应用,网络入侵检测、视频中行人异常行为检测、欺诈检测等都是异常值检测的应用。异常值检测方法也分为有监督,无监督和半监督方法,这里不再详细展开。

    2. 数据融合

    所谓数据融合就是将不同来源的、异质的数据融合到一起。良好的数据融合可以减少数据中的冗余(redundacies)和不一致性(inconsistence),进而提升后续步骤的精度和速度。数据融合包括如下几个步骤:

    实体识别问题(Entity Identification Problem)

    实体识别中最主要的问题匹配不同的数据源中指向现实世界相同实体的纪录。比如分析有不同销售员纪录的14年和15年两年的销售数据,由于不同的销售员有不同的纪录习惯,顾客的名字纪录方式并不一样,一个销售员喜欢纪录全名(例如 Wardell Stephen Curry II),另外一个销售员喜欢将中间名省略(Wardell S Curry II ),虽然Wardell Stephen Curry II和Wardell S Curry II是现实世界中是同一名顾客,但计算机会识别为两位不同的顾客,解决这个问题就需要Entity Identification。一个常用的Entity Indentification Problem的解决算法是LSH算法
    另外一个问题是Schema integration, Schenma在这里指使用DBMS支持的形式化语言对一个数据库的结构化描述,Schema是构建一个数据库的蓝图。Schema intergration则是指,将若干个Schema合成一个global Schema,这个global Schema可以表达所有子Schema的要求(也就是一个总的蓝图)。属性的metadata(比如名称、取值范围、空值处理方法)可以帮助减少Schema Intergration的错误。

    冗余和相关性分析

    当能够从样本的一个或多个属性推导出另外的属性的时候,那么数据中就存在冗余。检测冗余的一种方法是相关性分析—-给定要进行检测的两个属性,相关性分析可以给出一个属性隐含(imply)另外一个属性的程度。对于标称型(Nominal)数据,可以使用$\chi^2$检验,而对于数值数据,可以根据方差和相关系数来分析。
  • 标称数据的$\chi^2$相关性检验。
    假设有A和B两个属性,A有$a_1,a_2,…a_c$共c种不同的取值,B有$b_1,b_2,…b_r$共r种不同的取值。我们可以为属性A和B建立一个列联表(contingency table)C,所谓列联表,就是一个r*c的矩阵,位置(i,j)代表属性A的值$a_i$和属性B的值$b_i$在样本中同时出现(事件(A=$a_i$,B=$b_j$)发生)的频率$o_ij$。属性A和属性B的$\chi^2$值可以通过下面的式子计算:

    $\chi^2=\sum_{i=1}^{c}\sum_{j=1}^{r}\frac{(o_{ij}-e_{ij})^2}{e_{ij}}$ (1)

其中$o_ij$是联合事件(A=$a_i$,B=$b_j$)发生的频率,$e_ij$是期望频率,用如下的公式计算:

$e_{ij}=\frac{count(A=a_i)\times count(B=b_j)}{n}$ (2)

  • 数值数据的相关系数
    假设有n个样本,$S_1,S_2,…S_n$,样本$S_i$的A,B两种属性的值分别是$a_i,b_i$,那么属性A和B的相关系数定义是:

    $r_{A,B}=\frac{\sum_{i=1}^{n}(a_i-\overline{A})(b_i-\overline{B})}{n\delta_A\delta_B}=\frac{\sum_{i=n}^{n}(a_i b_i)-n\overline{A} \overline{B}}{n\delta_A\delta_B}$ (3)

当相关系数是正的时候表示属性A和属性B正相关,当相关系数是负的时候属性A和属性B负相关,注意,相关关系并不等同于因果关系。

在torch中使用cuda进行训练

0. 说明

本文介绍hdf5文件的读取网络的定义训练测试,并强调了如何使用cuda对网络的训练、测试进行加速。

1. 过程与代码

  • 1 安装hdf5
    按照官方tutorial安装torch对hdf5格式文件的支持。
  • 2 引入必要 package 并读入hdf5数据

    1
    2
    3
    4
    5
    require 'torch';----如果是在itorch或itorch notbook中自动引入torch
    require 'hdf5';----hdf5支持
    require 'nn'; ----neural network modules 的实现
    require 'cutorch';----cuda backend支持
    require 'cunn';----neural network modules的cuda实现

    下面读入数据,假设train和test数据都是按照我上篇文章的格式所存储:

    1
    2
    3
    4
    5
    myFiletrian=hdf5.open('mytrain.h5','r')----读取hdf5文件
    myFiletest=hdf5.open('mytest.h5','r')
    ----将读入的hdf5存到两个Table: trainset和testset中
    trainset={data=myFiletrian:read('data'):all(),label=myFiletrian:read('label'):all():byte()}
    testset={data=myFiletest:read('data'):all(),label=myFiletest:read('label'):all():byte()}
  • 3 重载trainset的_index操作符并添加成员函数size()
    Lua中也存在面向对象的概念,Lua中的类可以通过Table+function模拟出来。这里将trainset视作一个类的对象,torch要求这个类有方法trainset:size()可以返回样本个数,使用操作符trainset[i]时返回第i个样本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    setmetatable(trainset, ----重载index操作符
    {__index = function(t, i)
    return {t.data[i], t.label[i]}
    end}
    );

    function trainset:size()----添加成员函数 size()
    return self.data:size(1)
    end
  • 4 建立一个网络
    我使用Torch的主要原因是因为Torch有3D conv模块,所以这里以模仿LeNet的3D conv网络为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    net=nn.Sequential()
    net:add(nn.VolumetricConvolution(3,5,3,3,3))----添加3D conv层,输入3个feature cube,输出5个feature cube,filter size为3*3*3
    net:add(nn.VolumetricMaxPooling(2,2,2,2,2,2))----添加3D MaxPooling层
    net:add(nn.VolumetricConvolution(5,16,4,4,4))
    net:add(nn.VolumetricMaxPooling(2,2,2,2,2,2))
    net:add(nn.View(16*4*4*4))----连接全连接层之前要使用nn.View()进行reshpe,nn.View()里面的参数需要根据
    ---(接上)数据的尺寸和前面的层来计算,比如在这里我是用的训练数据是3*24*24*24的cube,分别经过3*3*3和4*4*4 filter尺寸的两次卷积和两次maxpooling之后最终的输出是16个4*4*4*的feature cube ,那么nn.View()里面的参数就应该是16*4*4*4
    net:add(nn.Linear(16*4*4*4,120))
    net:add(nn.Linear(120,84))----nn.Linear(a,b),设置a个输入neurons和b个输出neurons的全全连接层
    net:add(nn.Linear(84,7))
    net:add(nn.LogSoftMax())-----输出每个类别概率P的log函数值log(P)
  • 5 建立一个Solver

    1
    2
    3
    4
    5
    criterion=nn.ClassNLLCriterion()----使用negatice log--likehood criterion ,即计算cross-entropy损失
    ----注意!Torch中训练数据的Label应该从1开始,比如有四类样本,那么label应该是1、2、3、4,不能从0开始,否则报错
    trainer=nn.StochasticGradient(net,criterion)----使用随机梯度下降
    trainer.learningRate=0.001----设置solver参数
    trainer.maxIteration=10----迭代epoch数
  • 6 将net、criterion、trainset和testset移动到GPU中
    如果要使用GPU加速,net、criterion、trainset和testset都需要移动到CPU内存中去

    1
    2
    3
    4
    5
    6
    trainset.data=trainset.data:cuda()
    trainset.label=trainset.label:cuda()
    testset.label=trainset.label:cuda()
    testset.data=testset.data:cuda()
    criterion=criterion:cuda()
    net=net:cuda()

以trainset为例,移动到GPU之后是形式如下的对象

{
data : CudaTensor - size: 10500x3x24x24x24
size : function: 0x42832ef0
label : CudaTensor - size: 10500x1
}

  • 7 训练并测试网络
    有了上面的准备,网络的训练十分简单:
    1
    trainer:train(trainset)

训练过程中输出如下:
Out[18]:StochasticGradient: training
Out[18]:current error = 1.9459465204875
Out[18]:current error = 1.941716547001
Out[18]:current error = 1.921697193475
Out[18]:current error = 1.8641934820811
Out[18]:current error = 1.5424795499416
Out[18]:current error = 1.3549344599815
Out[18]:current error = 1.2374953328995
Out[18]:current error = 1.152671923592
Out[18]:current error = 1.094293069022
Out[18]:current error = 1.0469601832571
StochasticGradient: you have reached the maximum number of iterations
training error = 1.0469601832571

由于设置的最大epoch是10,所以在对trainset迭代了10次之后训练就结束了,接下来是网络的测试:

1
2
3
4
5
6
7
8
9
10
correct = 0
for i=1,3500 do ----我的测试集有3500个cube
local groundtruth = testset.label[i][1]
local prediction = net:forward(testset.data[i])
local confidences, indices = torch.sort(prediction, true)
if groundtruth == indices[1] then
correct = correct + 1
end
end
print(correct, 100*correct/3500 .. ' % ')

2. 总结

使用Torch训练过程包括数据载入,网络定义,trainer定义,训练、测试几个部分,如果使用GPU加速,要将数据等对象移动到GPU中。