对应的论文地址:http://arxiv.org/abs/1312.4400
经典CNN中的卷积层其实就是用线性滤波器对图像进行内积运算,在每个局部输出后面跟着一个非线性的激活函数,最终得到的叫作特征图。而这种卷积滤波器是一种广义线性模型(GLM:(Generalized linear model))。所以用CNN进行特征提取时,其实就隐含地假设了特征是线性可分的,可实际问题往往是难以线性可分的。
GLM的抽象能力是比较低水平的,自然而然地我们想到用一种抽象能力更强的模型去替换它,从而提升传统CNN的表达能力。
如我们前面提到的Maxout是凸集可分的抽象模型,还有现在的NIN是另一种可以拟合任何函数的抽象模型,没有限制。
它是如何实现的?
选择一个多层感知机作为一个微网络结构,提取抽象能力更强的特征。
优点:
1.多层感知机也是使用BP算法进行训练的,可以与CNN进行整合;
2.多层感知机也可以作为一个深层结构,也包含了特征重用的思想;
3.多层感知机非常有效的通用函数近似器。
使用了这样一种微网络结构之后,那么就可以抽象出更好的局部特征,增强了局部模型的表达能力,使特征图具有与类别之间的一致性。
基于上面这样的一种考虑,在最后softmax的前一层,不进行全连接操作,而采用全局平均池化的方法将每一张特征图的平均值作为每个类别的置信度。
例如:一共有100类,那么在最后一层的输出feature map则为100,计算这100张feature map的平均值,作为这100类的置信度。
全局平均池化的好处:
1.通过加强特征图与类别的一致性,让卷积结构更简单,可以使用更少的feature map;(因为经典的CNN为了解决广义线性模型抽象能力不足的问题,采取过多的filter集合来弥补,也就是说不通的滤波器来检查同一特征的不同变体);
2.不需要进行参数优化,减少了参数,所以这一层可以避免过拟合;
3.它对空间信息进行了求和,因而对输入的空间变换更具有稳定性。
总结:
NIN的优点:
1.更好的局部抽象;
2.更小的全局Overfitting;
3.更少的参数(没有全连接层)
之前提到的Maxout对线性函数进行最大化处理可得到分段线性函数近似器,可近似任意的凸函数!它是假设特征位于凸集内,但是实际上很多时候都不可能,所以NIN结构的任意函数拟合具有优越性,有更好的抽象能力。
]]>对应的论文地址:http://arxiv.org/abs/1312.4400
经典CNN中的卷积层其实就是用线性滤波器对图像进行内积运算,在每个局部输出后面跟着一个非线性的激活函数,最终得到的叫作特征图。而这种卷积滤波器是一种广义线性模型(GLM:(Generalized linear model))。所以用CNN进行特征提取时,其实就隐含地假设了特征是线性可分的,可实际问题往往是难以线性可分的。
GLM的抽象能力是比较低水平的,自然而然地我们想到用一种抽象能力更强的模型去替换它,从而提升传统CNN的表达能力。
]]>
关于Maxout的论文为:http://jmlr.org/proceedings/papers/v28/goodfellow13.pdf
我们常见的隐含层节点输出:
$$h_i(x)=sigmoid(x^TW_{…i}+b_i)$$
而在Maxout网络中,其隐含层节点的输出表达式为:
$$h_i(x)=\max_{j\in[1,k]}z_{ij}$$
where$z_{ij}=x^TW_{…ij}+b_{ij}$,and $W\in{R^{dmk}}$
这里的W是3维的,尺寸为dmk,其中d表示输入层节点的个数,m表示隐含层节点的个数,k表示每个隐含层节点对应了k个”隐隐含层”节点,这k个”隐隐含层”节点都是线性输出的,而maxout的每个节点就是取这k个”隐隐含层”节点输出值中最大的那个值。因为激发函数中有了max操作,所以整个maxout网络也是一种非线性的变换。
类似下面这张图:
如果还是不太清楚的话,看这个渣手绘(请原谅我丑陋的绘画 /(ㄒoㄒ)/~~)
显然,Maxout是一个分段线性函数,而由于任意的凸函数都可由分段线性函数来拟合,所以,Maxout可以拟合任意的凸函数,论文里列出了以下几种图:
总结:Maxout能拟合任意凸函数,要与Dropout组合使用可以发挥比较好的效果。但是它加入了一个先验:样本集是凸集可分的。
]]>关于Maxout的论文为:http://jmlr.org/proceedings/papers/v28/goodfellow13.pdf
我们常见的隐含层节点输出:
$$h_i(x)=sigmoid(x^TW_{…i}+b_i)$$
而在Maxout网络中,其隐含层节点的输出表达式为:
$$h_i(x)=\max_{j\in[1,k]}z_{ij}$$
where$z_{ij}=x^TW_{…ij}+b_{ij}$,and $W\in{R^{dmk}}$
]]>
这段时间,让师弟研究了下RCNN的内容,我自己也看了一下,对RCNN的三大系列(还有Fast RCNN,Faster RCNN)做了一些了解,在这里简单做个笔记。
RCNN是目前目标检测这个task里可以说是最著名的了,包括后面的fast-rcnn和faster-rcnn都是同一个作者提出来的,使目标检测的实时性成为了可能。它的主要框架图是:
可以看出它的框架组成:
1.用selective search方法划分2k-45k个region;
2.分别对每个region提取特征,最后将提取到的特征送到k(k的取值与类别数相等)个svm分类器中识别以及送到一个回归器中去调节检测框的位置;
3.将k个SVM分类器中得分最高的类作为分类结果,将所有得分都不高的region作为背景;
4.通过回归器调整之后的结果即为检测到的位置。
这一部分就是利用selective search的方法来划分region,selective search方法是一个语义分割的方法,它通过在像素级的标注,把颜色、边界、纹理等信息作为合并条件,多尺度的综合采样方法,划分出一系列的区域,这些区域要远远少于传统的滑动窗口的穷举法产生的候选区域。如下图所示:
通过训练好的Alex-Net,先将每个region固定到227*227的尺寸,然后对于每个region都提取一个4096维的特征。作者提到了一点是在resize到227*227的过程中,在region外面扩大了一个16个宽度的边框,region的范围也就相应扩大,,考虑了更多的背景信息。作者在论文后面的Appendix A也有讨论。
首先拿到Alex-Net在imagenet上训练的CNN作为pre-train,然后将该网络的最后一个fc层的1000改为N+1(N为类别的数目,1是加一个背景)来fine-tuning用于提取特征的CNN。作者将大于0.5的IOU作为正样本,小于0.5的作为负样本,调整学习率为原来的1/10,对于每个batch-size = 128,32个为正样本,96个位负样本。
训练N(N为类别数)个lsvm分类器,分别对每一类做一个二分类,在这里,作者是将大于0.5的IOU作为正样本,小于0.3的IOU作为负样本,至于为啥这里不是0.5,当设置为0.5的时候,mAp下降5%,设置为0的时候下降4%,最后取中间0.3。作者在后面的Appendix B中有讨论,并且说softmax在VOC 2007上测试的mAP违纪50.9%,没有svm的54.2%的效果好。为啥呢?作何讨论说,因为softmax的负样本(也可以理解为背景样本)是随机选择的即在整个网络中是共享的,而svm的负样本是相互独立的,每个类别都分别有自己的负样本,svm的负样本更加的“hard”,所以svm的分类的准确率更高。
这一部分,作者是通过训练一个回归器来对region的范围进行一个调整,毕竟region最开始只是用selective search的方法粗略得到的,通过调整之后得到更精确的位置。
在PASCAL VOC2010-12上的结果
在ILSVRC13上的结果
在这里,作者是用pool5的特征来做可视化的,根据前面层的一些参数计算可知,p5层的一个单元是对应到227*227中原图的一个195*195的感受野,作者采用的可视化方法就是将10million个已经训练好的region做一个前向,用nonmaximum方法,并按照置信度分数排序,然后对应到原图中的单元中,看网络学到了什么,下图就是可视化的结果:
这是一些对比分析的实验结果:
1.Performance layer-by-layer, without fine-tuning
这个对比试验就是上图中的前三行,分别是用pool5,fc6和fc7这三层的特征做分类,结果都差不多,而且pool6的结果还比pool7的结果好,作者就得出结论是:CNN的特征表达一般是在卷积层。
2.Performance layer-by-layer, with fine-tuning
这个对比试验就是上图中的第4-6行,分别是pool5,fc6和fc7经过finetuning之后的结果,由上图可以看出,pool5经过finetuning之后,mAP的提高不大,所以可以说明卷积层提取出来的特征是更具有泛化性的,而fc7经过finetuning之后的提升最大,说明finetuning主要作用于全连接层。
3.Comparison to recent feature learning methods
这个对比试验就是上图中的最后三行,对比的是其他的特征,明显可以看出,CNN的特征学习能力比其他的方法要好。
这段时间,让师弟研究了下RCNN的内容,我自己也看了一下,对RCNN的三大系列(还有Fast RCNN,Faster RCNN)做了一些了解,在这里简单做个笔记。
RCNN是目前目标检测这个task里可以说是最著名的了,包括后面的fast-rcnn和faster-rcnn都是同一个作者提出来的,使目标检测的实时性成为了可能。它的主要框架图是:]]>
im2col_cpu
这个函数做的事,举例来说,一张2828的图像,20个55的卷积核,pad=0,stride=1,那么上面函数中的height_col和width_col的值都是24,channels_col = 155 = 25,所以可以得到data_col的大小为252424 = 14400。他的代码如下:1 | void im2col_cpu(const Dtype* data_im, const int channels, |
这个函数根据参数名比较好懂,知道了这个思想之后,我们来一步步的看下去,首先是forward函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void ConvolutionLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
const vector<Blob<Dtype>*>& top) {
const Dtype* weight = this->blobs_[0]->cpu_data();
for (int i = 0; i < bottom.size(); ++i) {
const Dtype* bottom_data = bottom[i]->cpu_data();
Dtype* top_data = top[i]->mutable_cpu_data();
for (int n = 0; n < this->num_; ++n) {
this->forward_cpu_gemm(bottom_data + n * this->bottom_dim_, weight,
top_data + n * this->top_dim_);
if (this->bias_term_) {
const Dtype* bias = this->blobs_[1]->cpu_data();
this->forward_cpu_bias(top_data + n * this->top_dim_, bias);
}
}
}
}
这个weight数组的weight_size = 卷积核个数*卷积核width*卷积核height,num_为batch_size的值。这个函数比较好理解,分别对batch批次图片进行卷积操作,forward_cpu_gemm
是计算 weight与图片之间进行相乘,如果有偏置项的话,那么 forward_cpu_bias
就是在刚才计算的基础上再加一个偏置,下面我们来看一看forward_cpu_gemm
函数做了一些什么:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void BaseConvolutionLayer<Dtype>::forward_cpu_gemm(const Dtype* input,
const Dtype* weights, Dtype* output, bool skip_im2col) {
const Dtype* col_buff = input;
if (!is_1x1_) {
if (!skip_im2col) {
conv_im2col_cpu(input, col_buffer_.mutable_cpu_data());
}
col_buff = col_buffer_.cpu_data();
}
for (int g = 0; g < group_; ++g) {
caffe_cpu_gemm<Dtype>(CblasNoTrans, CblasNoTrans, conv_out_channels_ /
group_, conv_out_spatial_dim_, kernel_dim_,
(Dtype)1., weights + weight_offset_ * g, col_buff + col_offset_ * g,
(Dtype)0., output + output_offset_ * g);
}
}
在这个函数中,weight_offset_ = channels*卷积核个数*卷积核width*卷积核height,col_offset_的值就是我在im2col_cpu
中计算的data_col的大小,output_offset_ = 卷积核个数*卷积后的图片宽度*卷积后的图片长度。conv_im2col_cpu
这个函数里面是直接调用了我最上面提到的im2col_cpu
函数,我也把代码贴一下吧:1
2
3
4
5
6
7
8
9
10
11
12
13inline void conv_im2col_cpu(const Dtype* data, Dtype* col_buff) {
if (!force_nd_im2col_ && num_spatial_axes_ == 2) {
im2col_cpu(data, conv_in_channels_,
conv_input_shape_.cpu_data()[1], conv_input_shape_.cpu_data()[2],
kernel_shape_.cpu_data()[0], kernel_shape_.cpu_data()[1],
pad_.cpu_data()[0], pad_.cpu_data()[1],
stride_.cpu_data()[0], stride_.cpu_data()[1], col_buff);
} else {
im2col_nd_cpu(data, num_spatial_axes_, conv_input_shape_.cpu_data(),
col_buffer_shape_.data(), kernel_shape_.cpu_data(),
pad_.cpu_data(), stride_.cpu_data(), col_buff);
}
}
而那个group循环是用来做group convolution的,默认group_的值为1,不过据caffe作者自述,group convolution是没什么用的,大家只需要知道,在这里,只会循环一次即可。
那么在caffe_cpu_gemm里面到底又做了些什么呢?
1 | void caffe_cpu_gemm<double>(const CBLAS_TRANSPOSE TransA, |
这个函数的作用就是将刚才在im2col_cpu
中生成的data_col数组调用 cblas_dgemm
函数转换为矩阵相乘。
对参数说明一下,TransA和TransB都是CblasNoTrans,可知lda = K,ldb = N,M的值为num_output,即为卷积核的个数,N为out_put_size,即为经过卷积之后图片的尺寸,K=channels kernel_h kernel_w,就是我们在im2col_cpu
函数中计算的channels_col
值,可以把它理解为一个三维的卷积核的大小,alpha为1,A为weights
,B为在im2col_cpu
中计算的data_col
,C就是计算后的值,而cblas_dgemm
函数就是将A和B数组转换为KN和KN的矩阵做点积运算,A为参数矩阵,是将M个channels kernel_h kernel_w的三维卷积核拉成一个列向量,然后复制N个列向量组成A,B为channels个图像待卷积的小块都转换成列向量而组成的,C就是卷积之后的结果,也为K*N。cblas_dgemm
函数是blas的一个函数,这里简单的给出它的一些说明:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18Declaration
SWIFT
func cblas_dgemm(_ Order: CBLAS_ORDER,
_ TransA: CBLAS_TRANSPOSE,
_ TransB: CBLAS_TRANSPOSE,
_ M: Int32,
_ N: Int32,
_ K: Int32,
_ alpha: Double,
_ A: UnsafePointer<Double>,
_ lda: Int32,
_ B: UnsafePointer<Double>,
_ ldb: Int32,
_ beta: Double,
_ C: UnsafeMutablePointer<Double>,
_ ldc: Int32)
OBJECTIVE-C
void cblas_dgemm ( const enum CBLAS_ORDER __ Order , const enum CBLAS_TRANSPOSE __ TransA , const enum CBLAS_TRANSPOSE __ TransB , const int __ M , const int __ N , const int __ K , const double __ alpha , const double *__ A , const int __ lda , const double *__ B , const int __ ldb , const double __ beta , double *__ C , const int __ ldc );
以上所述就是caffe中的卷积操作,这里还有一张图,可以更好的辅佐理解一下
im2col_cpu
这个函数做的事,举例来说,一张2828的图像,20个55的卷积核,pad=0,stride=1,那么上面函数中的height_col和width_col的值都是24,channels_col = 155 = 25,所以可以得到data_col的大小为252424 = 14400。他的代码如下:1 | void im2col_cpu(const Dtype* data_im, const int channels, |
它的主要思想是:
对于一个哈希码h,一共有b位,将它分为m段,那么,每段的长度为$\lfloor b/m\rfloor$或者$\lceil b/m \rceil$,为了方便起见,我们假设b能够被m整除,下位直接用$b/m$代替,那么当两个哈希码h和g的汉明距离为r bits的时候,在这m段哈希码中必然至少存在一段,它们之间的汉明距离最多为$\lfloor r/m \rfloor$。
那么基于这个思想,我们可以在查询与哈希码h的汉明距离为r的所有哈希码时,我们可以分别对这m段哈希码进行查找,找出在每一段哈希码中,汉明距离小于$\lfloor r/m \rfloor$的查询结果,将m段的查询结果合并之后就是我们最终的候选集,最后在候选集中筛选出汉明距离大于r的结果。这种方法相比于线性搜索的需要查找$L(b,r)$个桶的情况,现在我们只需要搜索$m*L(b/m,\lfloor r/m \rfloor)$个哈希桶,大大提高了查询效率。
查询的cost,每次查询需要查找哈希桶的查找次数为
存储cost,考虑一共有n个b位的哈希码,分为m段,那么就有m个哈希table,存储所有的哈希码就需要$O(nb)$,对于m个哈希表,我们存储着n个哈希码的索引,那么对于n个哈希码来说还额外需要$O(mnlog_2n)$bits,所以,存储cost一共需要$O(nb+mnlog_2n)$。
上面所说的是r-neighbors查询,但是对于实际的需要中,我们很多时候不关心r,我们关心的是返回查询结果的前k个,那么这种情况该怎么做呢?论文首先通过以下这种图来说明在不同取值k的情况下,汉明距离分布的不均匀性。
这篇论文的思想与前面所提到的的simhash的方法很相似,只不过simhash是对前面的一部分精确匹配,这篇论文是对每一段都是模糊匹配,方法较linear scan有较大的效率提升,并且存储的复杂度也不是特别高,作者也把实现代码开源了。
]]>它的主要思想是:
对于一个哈希码h,一共有b位,将它分为m段,那么,每段的长度为$\lfloor b/m\rfloor$或者$\lceil b/m \rceil$,为了方便起见,我们假设b能够被m整除,下位直接用$b/m$代替,那么当两个哈希码h和g的汉明距离为r bits的时候,在这m段哈希码中必然至少存在一段,它们之间的汉明距离最多为$\lfloor r/m \rfloor$。
那么基于这个思想,我们可以在查询与哈希码h的汉明距离为r的所有哈希码时,我们可以分别对这m段哈希码进行查找,找出在每一段哈希码中,汉明距离小于$\lfloor r/m \rfloor$的查询结果,将m段的查询结果合并之后就是我们最终的候选集,最后在候选集中筛选出汉明距离大于r的结果。这种方法相比于线性搜索的需要查找$L(b,r)$个桶的情况,现在我们只需要搜索$m*L(b/m,\lfloor r/m \rfloor)$个哈希桶,大大提高了查询效率。
]]>
引用这张经典的原理图:
它的算法流程如下:
1.选择simhash的位数,例如32位或64位;
2.将Doc文件进行分词,并赋予权重,赋予权重的方法有很多重,可以根据词频等信息的一些方法。将每个关键词转换为对应位数的哈希码,在这里,我们假设我们的哈希位数为6,那么依据图中信息可知n个词的哈希码如上面100110,110000…;
3.然后对这n个哈希码按列相加,如果哈希码为1,那么+weight
,反之-weight
,计算最后生成的结果,如上图所示的[13,108,-22,-5,-32,55];
4.然后由[13,108,-22,-5,-32,55]转换成最终的哈希码[1,1,0,0,0,1],正取1,负取0。
好,以上就是我们生成哈希码的过程,那么得到哈希码之后,如何从海量文本中查询汉明距离为r的文本呢?
假设在这里,我们要查询汉明距离为3以内的数据,那么只要我们将整个64位的哈希码分为4块,无论如何,在满足汉明距离为3的情况下,至少有一段两个哈希码是完全相同的。
所以基于以上思想,在刚才那个例子的情况下,我们可以设计出如下算法,将64位哈希码分成4份,并需要将这64位哈希码存储为4份table,分别变换精确匹配的位置,来查找前16位哈希码完全相同的记录作为候选记录,如下图所示:
具体的算法过程如下:
1.将64为哈希码分为4份;
2.调整这4段哈希码的位置,分别将这四种情况存储在4分table中;
3.采用精确匹配的方法查找前16位哈希码
4.如果样本库中存有$2^{34}$(差不多10亿)的哈希指纹,则每个table返回$2^{(34-16)}=262144$个候选结果,大大减少了海明距离的计算成本。
那么有人可能会问,那分为5段的话又是怎么查询呢?
分为5段的话,那么需要$C_5^3=10$个哈希表来存储,每段哈希码大约为$64/5=13$位,分别变换这5段哈希码的位置,存储为10份哈希表,每次精确匹配前两段,那么10个哈希表分别会返回$2^{(34-13*2)}$个结果。同理,分为6段的话,那么需要$C_5^3$=20个哈希表来存储,每段哈希码大约为$64/6=11位哈希码$,那么每段返回的结果会更少。
可知,时间与空间的效率不可兼得,选择一个合适的分段数是很重要的。
另外,这里还提供一个比较两段哈希码汉明距离的代码:1
2
3
4
5
6
7
8
9
10
11int calHammingDis(uint64_t lhs, uint64_t rhs)
{
int cnt = 0;
lhs ^= rhs;
while(lhs)
{
lhs &= lhs - 1;
cnt++;
}
return cnn;
}
其实在这里,我们看到simhash的一个弊端,它的分段数必须大于查询的汉明距离,不然就失去了作用。
另外,还写了一篇multi-index-hash的文章的思想与这篇的思想很像,可以一起对照来看。
[1]: http://grunt1223.iteye.com/blog/964564
[2]: http://yanyiwu.com/work/2014/01/30/simhash-shi-xian-xiang-jie.html
[3]: http://tangxman.github.io/mih/
引用这张经典的原理图:
它的算法流程如下:
1.选择simhash的位数,例如32位或64位;
2.将Doc文件进行分词,并赋予权重,赋予权重的方法有很多重,可以根据词频等信息的一些方法。将每个关键词转换为对应位数的哈希码,在这里,我们假设我们的哈希位数为6,那么依据图中信息可知n个词的哈希码如上面100110,110000…;
3.然后对这n个哈希码按列相加,如果哈希码为1,那么+weight
,反之-weight
,计算最后生成的结果,如上图所示的[13,108,-22,-5,-32,55];
4.然后由[13,108,-22,-5,-32,55]转换成最终的哈希码[1,1,0,0,0,1],正取1,负取0。
好,以上就是我们生成哈希码的过程,那么得到哈希码之后,如何从海量文本中查询汉明距离为r的文本呢?
]]>
Hesse矩阵在牛顿法中常被用到,它的本质就是二维偏导的矩阵,形式如下所示:
之前介绍的梯度下降法,只考虑了目标函数的一阶导数信息,而牛顿法择一并考虑二阶导数信息。
设$f(x)$为具有二姐连续偏导数的目标函数,第$k$次迭代值为$x_k$,在$x_k$处进行二阶泰勒展开:
$$f(x)=f(x_k)+g_k^T(x-x_k)+\frac{1}{2}(x-x_k)^TH(x_k)(x-x_k)$$
$g_k$是$f(x)$的梯度向量在点$x_k$处的值,$H(x_k)$是$f(x)$的Hesse矩阵在点$x_k$处的值。
由于极小值点必然是一阶导数为0的点,所以我们令$f(x)$的一阶导数等于0,求上面那个泰勒展开式的导数:
$$\nabla{f(x)}=g_k+H_k(x-x_k)$$
那么,由$\nabla{f(x)}=0$,得
$$g_k+H_k(x-x_k)=0$$
可知,当$H_k$为非奇异矩阵的时候,它的逆矩阵存在,两边同乘逆矩阵,迭代公式即为
$$x=x_k-H_k^{-1}g_k=x_k+d$$
$$H_kd=-g_k$$
用上面方法作为迭代公式的算法就是牛顿法,一般认为牛顿法可以利用到曲线本身的信息,比梯度下降法更容易收敛(迭代次数更少),下图就是一个最小化目标方程的例子,红色曲线是利用牛顿法迭代求解,绿色曲线是利用梯度下降法求解。
这种牛顿法虽然具有二次收敛性,但是要求初始点需要尽量靠近极小点,否则有可能不收敛。计算过程中需要不断计算目标函数的二阶偏导数,计算量大。而且Hesse矩阵无法一直保持正定,会导致算法产生的方向不能保证是$f(x)$在$x_k$处的下降方向,从而牛顿法失效(只有Hesse矩阵正定,才能保证$f_x$在$x_k$处下降)。
正是由于牛顿法上面的这些限制,所以才有了拟牛顿法。
拟牛顿法的基本思想是不计算二阶偏导数,构造出一个近似Hesse的逆矩阵的正定对称阵,从而根据这个近似矩阵来优化目标函数。不同的近似阵构造方法决定了不同的拟牛顿法。
设$f(x)$二次连续可微,$f$在$x_{k+1}$附近的泰勒展开近似即为
$$f(x)=f(x_{k+1})+g_k^T(x-x{k+1})+\frac{1}{2}(x-x{k+1})^TH_{k+1}(x-x_{k+1})$$
两边同时求导
$$g(x)=g_{k+1}+H_{k+1}(x-x_{k+1})$$
取$x=x_k$
$$g_k=g_{k+1}+H_{k+1}(x_k-x_{k+1})$$
$$g_k-g_{k+1}=H_{k+1}(x_k-x_{k+1})$$
记$y_k=g_{k+1}-g_k$,$\delta_k=x_{k=1}-x_{k}$
$$y_k=H_{k+1}\delta_k$$
或
$$H_{k+1}^{-1}y_k=\delta_k$$
上面两个公式即为拟牛顿条件。按照拟牛顿条件选择$G_k$作为$H_k^{-1}$的近似或者选择$B_k$作为$H_k$的近似的算法称为拟牛顿法。
按照拟牛顿条件来更新矩阵$G_{k+1}$
$$G_{k+1}=G_k+\Delta{G_k}$$
更新$G_k$之后,将$G_k$作为$H^{-1}$的近似或者$B_k$作为$H$的近似,然后根据$H_kd=-g_k$,即可算出$d$的值。
DFP算法选择$G_{k+1}$的方法是在$G_k$加上两个附加项构成的,即
$$G_{k+1}=G_k+P_k+Q_k$$
其中,$P_k$,$Q_k$是待定矩阵,为了使$G_{k+1}$满足拟牛顿条件,可使$P_k$和$Q_k$满足:
$$P_ky_k=\delta{k}$$
$$Q_ky_k=-G_ky_k$$
具体的求解过程此处从略,可取
$$P_k=\frac{\delta{_k}\delta{_k}^T}{\delta{_k}^Ty_k}$$
$$Q_k=-\frac{G_ky_ky_k^TG_k}{y_k^TG_ky_k}$$
这样就得到了矩阵$G_{k+1}$的迭代公式:
$$G_{k+1}=G_k+\frac{\delta{_k}\delta{_k}^T}{\delta{_k}^Ty_k}-\frac{G_ky_ky_k^TG_k}{y_k^TG_ky_k}$$
称为DFP算法。
可以考虑用$B_k$来逼近Hesse矩阵$H$,这时,拟牛顿条件为
$$B_{k+1}\delta{_k}=y_k$$
同样的,
$$B_{k+1}=B_k+P_k+Q_k$$
$$P_k\delta{_k}=y_k$$
$$Q_k\delta{_k}=-B_k\delta{_k}$$
找出符合条件的$P_k$和$Q_k$,得到BFGS算法矩阵的迭代公式:
$$B_{k+1}=B_k+\frac{y_ky_k^T}{y_k^T\delta_k}-\frac{B_k\delta_k\delta_k^TB_k}{\delta_k^TB_k\delta_k}$$
(1).http://blog.csdn.net/chlele0105/article/details/38895711
(2).http://www.codelast.com/?p=2780
Hesse矩阵在牛顿法中常被用到,它的本质就是二维偏导的矩阵,形式如下所示:
之前介绍的梯度下降法,只考虑了目标函数的一阶导数信息,而牛顿法择一并考虑二阶导数信息。
设$f(x)$为具有二姐连续偏导数的目标函数,第$k$次迭代值为$x_k$,在$x_k$处进行二阶泰勒展开:
$$f(x)=f(x_k)+g_k^T(x-x_k)+\frac{1}{2}(x-x_k)^TH(x_k)(x-x_k)$$
$g_k$是$f(x)$的梯度向量在点$x_k$处的值,$H(x_k)$是$f(x)$的Hesse矩阵在点$x_k$处的值。
由于极小值点必然是一阶导数为0的点,所以我们令$f(x)$的一阶导数等于0,求上面那个泰勒展开式的导数:
$$\nabla{f(x)}=g_k+H_k(x-x_k)$$
]]>
那么到底什么是无约束最优化呢?这里举一个栗子来说明一下,看下面这张图。
梯度下降法(或最速下降法)是求解无约束优化问题的一种最常用的方法,假设$f(x)$是$R^*$上具有一阶连续偏导数的函数,需要求解的无约束最优化问题是
$$\min_{x\varepsilon R^n}f(x)$$
$x^*$是目标函数的极小值点.
梯度下降法是一种迭代算法,选取适当的初值$x^{(0)}$,反复迭代,更新$x$的值,进行目标函数的极小化,直至收敛。由于我们都知道梯度方向是函数增长最快的方向,那么自然而然的想到负梯度方向就是函数值下降最快的方向了。因此,我们以负梯度方向作为极小化的下降方向,在迭代的每一步,以负梯度方向来更新$x$的值,从而达到减小函数值目的,这种方法就是梯度下降法(也叫最速下降法)。
由于$f(x)$具有一阶连续偏导数,若第$k$次迭代值为$x^{(k)}$,则可将$f(x)$在$x{(k)}$处进行一阶泰勒展开:
$$f(x)=f(x^{(k)})+g_k^T(x-x^{(k)})$$
这里,$g_k=g(x^{(k)})=\nabla{f(x^{(k)})}$为$f(x)$在$x^{(k)}$的梯度。
第$k+1$次迭代值$x^{(k+1)}$:
$$x^{(k+1)} = x^{(k)}+\lambda_kp_k$$
其中,$p_k$是搜索方向,取负梯度方向$p_k$=$-\nabla{f(x^{(x)})}$,$\lambda_k$是步长,有时候我们也叫学习率,这个值可以由一维搜索确定,即$\lambda_k$使得
$$f({x^{(k)}+\lambda_kp_k})=\min_{\lambda\geq0}f({x{(k)}+\lambda p_k})$$
即令导数为零找到该方向上的最小值,但是在实际编程中,这样计算的代价太大,我们一般可以将它设定为一个常量。
如果目标函数是一个凸优化问题,那么最速下降法得到的是全局最优解,否则,它有可能求得的只是一个局部最优解,理想的优化效果如下图所示。注意:每一次迭代的移动方向都与出发点的等高线垂直:
需要注意的是,最速下降法是每次迭代都需要计算全部样本值的梯度的,而在实际求解过程中,它的收敛速度是比较慢的,是一阶收敛。所以就出现了下面两种方法。
要了解共轭梯度下降法,首先要知道什么是共轭。设$G$为对称正定矩阵,若$a_m^TGa_n$,则称$a_m$和$a_n$为“$G$共轭”。当G为单位向量时,有$a_ma_n=0$,所以“共轭”是“正交”的推广。
那么沿着一系列的共轭方向做迭代,这些共轭方向组成的集合叫做共轭方向集,这既是共轭方向法。即对于二次正定目标函数,从任意点$x_0$出发,沿任意下降方向$a_m$做直线搜索得到$x_1$,沿与$a_m$共轭的方向$a_n$做直线搜索,反复迭代,即可得到目标函数的极小值点。
另外,还有一个概念要搞清楚,就是二次收敛性。
当目标函数是二次函数时,共轭方向法最多经过N步(N为向量维数)迭代,就可以到达极小值点——这种特性就是二次收敛性。(注:最下降法和随机梯度下降法为一次收敛)
那么,是不是当目标函数不是二次函数的时候,共轭方向法就失效了呢?答案是否定的,可以证明,将二次收敛算法用于非二次的目标函数时,也有很好的效果。但是,这个时候就不能保证N步迭代到达极小值了。
关于迭代的方式跟前面的最速下降法类似,在此不再赘述,这里的关键问题是如何产生一组关于$G$共轭的向量,有个常用的方法是Gram-Schmidt的方法。
取线性无关的向量组$v_0,v_1,v_2,…,v_{n-1}$,取$p_0=v_0$
$$p_{k+1}=v_{k+1}-\sum_{j=0}^k \frac{(p_i)^TGv_{k+1}}{(p_i)^TQp_i}p_i$$
上面的方法是针对目标函数为正定二次函数的,而对于一般的非二次函数,可以通过泰勒展开的二次近似。
而共轭梯度法是共轭方向法的一种延伸,初始的共轭向量$p_0$由初始点的负梯度$-g_0$给出,以后的$p_k$由当前迭代点的负梯度与上一个共轭向量的线性组合来确定:
$$p_{k+1}=-g_{k+1}+a_kp_k$$
$a_k$是迭代步长,$a_k=\frac{\left | g_{k+1} \right |^2}{\left |g_k \right |^2}$,对于非二次函数的优化问题,迭代次数不止n次,但共轭方向只有n个。当迭代n次后,可以把$p_n$ 重新置为最开始的 $p_0$,其他的变量按照原方法更新。
说了最速下降法,共轭梯度下降法,那么随机梯度下降法又是什么呢?随机梯度法是在深度学习里面用的最多的一种优化方法,为什么深度学习用的最多的是这种方法而不是最速下降法或者共轭梯度法呢?下面就来解释一下这个问题。
与最速下降法相比,最速下降法每次迭代都要计算所有训练集的梯度,如果训练集很大,可想而知,这种方法的效率会非常的低下,所以随机梯度下降法应运而生,随机梯度下降法是通过每次计算一个样本来迭代更新,那么可能只需要几百或者几千个样本,就可以得到最优解了,相比于上面的最速下降法和共轭梯度法,迭代一次需要全部的样本,这种方法效率自然就较高。
但是,与此同时,随机梯度下降法由于每次计算只考虑一个样本,使得它每次迭代并不一定都是整体最优化方向。如果噪声较多,很容易陷入局部最优解。
(1)http://www.cnblogs.com/daniel-D/p/3377840.html
(2)http://www.codelast.com/?p=8006
(3)http://www.codelast.com/?p=2348
那么到底什么是无约束最优化呢?这里举一个栗子来说明一下,看下面这张图。
主要区别:
map:它是有序的,内部实现是一颗红黑树,相比unordered_map内存消耗少,因为unordered_map有一个巨大的数组,也就是哈希表,会有很多空间的浪费。
unordered_map:无序,通过哈希函数来生成key,插入和删除,查询的效率更高,但是需要注意的一点是如果提前不知道unordered_map的size,没有reserve的话,那么插入的时候会经常不断的reserve,那么所有的元素都需要rehash,这是比较耗时的。
还有一个需要注意的地方,map比unorderd_map要更加稳定,在循环迭代输出的时候,map要比unordered_map更快。
用法:
由于map是按照Key值有序的,所以map中作为key的类型必须要重载”<”,例如自定义类型: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#include <string>
#include <map>
using namespace std;
struct Person {
string name;
int age;
Person(string name_, int age_):name(name_)
, age(age_) {}
bool operator<(const Person& p) const {
return this->age < p.age;
}
};
int main()
{
map<Person, string> map;
map.emplace(Person("txm", 23), "huster");
return 0;
}
如果不重载”<”,则会报错,或者这样,声明一个比较器: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#include <string>
#include <map>
using namespace std;
struct Person {
string name;
int age;
Person(string name_, int age_):name(name_)
, age(age_) {}
};
struct personCmp {
bool operator()(const Person& p,const Person &q) const {
return p.age < q.age;
}
};
int main()
{
map<Person, string,personCmp> map;
map.emplace(Person("txm", 23), "huster");
return 0;
}
那么,该如何实现按照value排序呢?
这里只能借助其他方法了,用库函数自带的sort()方法,sort()的定义是:
在第二个方法中,也可以自定义分类器,那么可以这样: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#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
#include <algorithm>
using namespace std;
struct Person {
string name;
int age;
Person(string name_, int age_):name(name_)
, age(age_) {}
};
typedef pair<Person, int> PAIR;
struct personCmp {
bool operator()(const Person& p,const Person &q) const {
return p.age < q.age;
}
};
struct PAIRCmp {
bool operator()(const PAIR& P, const PAIR& q) const {
return P.second < q.second;
}
};
int main()
{
map<Person, int, personCmp> map;
map.emplace(Person("txm", 2), 23);
map.emplace(Person("zoe", 3), 18);
map.emplace(Person("zy", 1), 19);
for (auto& p : map) {
cout << p.first.name << " : " << p.first.age << "age = " << p.second << endl;
}
cout << "--------------" << endl;
vector<PAIR> map_vec(map.begin(), map.end());
sort(map_vec.begin(), map_vec.end(), PAIRCmp());
for (auto& p : map_vec) {
cout << p.first.name << " : " << p.first.age << "age = " << p.second << endl;
}
return 0;
}
分上述程序输出:
zy : 1 age = 19
txm : 2 age = 23
zoe : 3 age = 18
-————————
zoe : 3 age = 18
zy : 1 age = 19
txm : 2 age = 23
unordered_map的用法
而unordered_map是哈希表的机制,也就是每个key会生成一个哈希码,根据哈希码来判断元素是否相同,故必须提供产生哈希码的函数,但是哈希码相同并不一定代表两个元素相同,所以还要实现”==”操作符。所以,可以这样: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#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
using namespace std;
struct Person {
string name;
int age;
Person(string name_, int age_):name(name_)
, age(age_) {}
bool operator==(const Person& p) const {
return (this->age == p.age && this->name == p.name);
}
};
namespace std {
template <>
struct hash<Person> {
size_t operator()(const Person &p) const {
return hash<string>()(p.name) ^ hash<int>()(p.age);
}
};
}
int main()
{
unordered_map<Person, string> uno_map;
uno_map.emplace(Person("zoe", 23), "whuer");
return 0;
}
上面这种方法是在key的类型里实现了”==”,重载了std的hash函数。还可以下面这样: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#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
using namespace std;
struct Person {
string name;
int age;
Person(string name_, int age_):name(name_)
, age(age_) {}
bool operator==(const Person& p) const {
return (this->age == p.age && this->name == p.name);
}
};
struct hashGenerator {
size_t operator()(const Person& p) const {
return hash<string>()(p.name) ^ hash<int>()(p.age);
}
};
int main()
{
unordered_map<Person, string, hashGenerator> uno_map;
uno_map.emplace(Person("zoe", 23), "whuer");
return 0;
}
将哈希码生成器定义为一个新的类,声明一下即可,当然也可以将”==”的也定义为一个新的类: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#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
using namespace std;
struct Person {
string name;
int age;
Person(string name_, int age_):name(name_)
, age(age_) {}
};
struct hashGenerator {
size_t operator()(const Person& p) const {
return hash<string>()(p.name) ^ hash<int>()(p.age);
}
};
struct personCmp {
bool operator()(const Person& p,const Person& q) const {
return (p->age == q.age && p->name == q.name);
}
};
int main()
{
unordered_map<Person, string, hashGenerator, personCmp> uno_map;
uno_map.emplace(Person("zoe", 23), "whuer");
return 0;
}
那么由上可以看出哈希函数的选择至关重要,哈希函数的好坏直接影响性能,比较推荐的做法是用boost库里的hash_value和hash_combine这两个函数,其实hash_value这个函数跟标准库里的hash1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#include <boost/functional/hash.hpp>
struct KeyHasher
{
std::size_t operator()(const Person& p) const
{
using boost::hash_value;
using boost::hash_combine;
// Start with a hash value of 0 .
std::size_t seed = 0;
// Modify 'seed' by XORing and bit-shifting in
// one member of 'Key' after the other:
hash_combine(seed,hash_value(p.name));
hash_combine(seed,hash_value(p.age));
// Return the result.
return seed;
}
};
主要区别:
map:它是有序的,内部实现是一颗红黑树,相比unordered_map内存消耗少,因为unordered_map有一个巨大的数组,也就是哈希表,会有很多空间的浪费。
unordered_map:无序,通过哈希函数来生成key,插入和删除,查询的效率更高,但是需要注意的一点是如果提前不知道unordered_map的size,没有reserve的话,那么插入的时候会经常不断的reserve,那么所有的元素都需要rehash,这是比较耗时的。
还有一个需要注意的地方,map比unorderd_map要更加稳定,在循环迭代输出的时候,map要比unordered_map更快。
]]>
QString 是不存在中文支持问题的,很多人遇到问题,并不是本身 QString 的问题,而是没有将自己希望的字符串正确赋给QString。
“我是中文”这样写的时候,它是传统的 char 类型的窄字符串,我们需要的只不过是通过某种方式告诉QString 这四个汉字采用的那种编码。而问题一般都出在很多用户对自己当前的编码没太多概念。
在这里介绍一篇对字符编码讲解的很透彻的一篇文章:ASCII、Unicode、GBK和UTF-8字符编码的区别联系
QString的内部是使用的unicode编码,所以本质就是将所有字符转换成unicode编码即可正常显示。
所以当使用这样的代码:1
QString a = "我是中文"
其实等价于1
2const char* s = "我是中文"
QString a = s;
那么当需要从窄字符串char转成Unicode的QString字符串时,你需要告诉Qt你的这个char 是什么编码?GBK、BIG5、Latin-1.
所以关于Qt的中文乱码,Qt4和Qt5有不同的解决方案。
在Qt4中,常用的解决方案是在main.cpp加这几行代码1
2
3
4QTextCodec *codec = QTextCodec::codecForName("system");//获取系统中文编码
QTextCodec::setCodecForLocale(codec);
QTextCodec::setCodecForCStrings(codec);
QTextCodec::setCodecForTr(codec);
这几行代码就是告诉程序你的char* 中到底使用的是什么编码。
而在Qt5中取消了这几个函数,取而代之的是另外的解决方案。
用QTextCodec类中的转换函数1
2
3
4
5
6
7
8
9
10
11std::string utf82gbk(const QString &inStr)
{
QTextCodec *gbk = QTextCodec::codecForName("GB18030");
return gbk->fromUnicode(inStr).data();
}
QString gbk2utf8(const std::string &inStr)
{
QTextCodec *gbk = QTextCodec::codecForName("GB18030");
return gbk->toUnicode(inStr.c_str());
}
QString是unicode串,对应QChar为2个字节;
string一般如果不包含中文则是ascii串,包含中文会自动转换成gbk串,对应字符char 1个字节;
wstring是宽字符,也是unicode串,对应wchar_t 2字节(或4字节,linux);
1 | QString q1 = QObject::tr("abc哈哈"); |
在上面代码中,len1和len3都为5,返回的是字符数,而len2为7返回的是字节数。
下面是三者之间的互相转换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//QString - string
string str = qstr.toStdString();
QString qstr(str.c_str());
//QString - wstring
wstring wstr = qstr.toStdWString();
QString qstr = QString::fromStdWString(wstr);
//string - wstring
//不用C++11的方案
// 需包含locale、string头文件、使用setlocale函数。
wstring StringToWstring(const string str)
{// string转wstring
unsigned len = str.size() * 2;// 预留字节数
setlocale(LC_CTYPE, ""); //必须调用此函数
wchar_t *p = new wchar_t[len];// 申请一段内存存放转换后的字符串
mbstowcs(p,str.c_str(),len);// 转换
wstring str1(p);
delete[] p;// 释放申请的内存
return str1;
}
string WstringToString(const wstring str)
{// wstring转string
unsigned len = str.size() * 4;
setlocale(LC_CTYPE, "");
char *p = new char[len];
wcstombs(p,str.c_str(),len);
string str1(p);
delete[] p;
return str1;
}
//C++11 现在提供了专门的转换函数
wstring s2ws(const std::string& str)
{
typedef std::codecvt_utf8<wchar_t> convert_typeX;
std::wstring_convert<convert_typeX, wchar_t> converterX;
return converterX.from_bytes(str);
}
string ws2s(const std::wstring& wstr)
{
typedef std::codecvt_utf8<wchar_t> convert_typeX;
std::wstring_convert<convert_typeX, wchar_t> converterX;
return converterX.to_bytes(wstr);
}
1 | //检查文件的后缀名 |
现有的C++ Excel处理库中,开源的解决方案中,没有可以支持.xls和.xlsx这两种文件的,所以我选取了两个库都分别处理,先判断他的后缀名,最后分析处理。
.xls的处理库我选择的是BasicExcel,但是不支持中文,需要进去源码里面去修改编码。
.xlsx的处理库选择的是Qt Xlsx,这个对中文支持较好,对xlsx文档的读写都比较友好,文档也很详细。
比较两个字符串,忽略大小写1
2
3
4
5
6
7
8
9
10
11bool iequal(const string& str1, const string& str2) {
if (str1.size() != str2.size()) {
return false;
}
for (string::const_iterator c1 = str1.begin(), c2 = str2.begin(); c1 != str1.end(); ++c1, ++c2) {
if (tolower(*c1) != tolower(*c2)) {
return false;
}
}
return true;
}
这个小工具本身比较简单,几天就写完了,但是又很多小地方需要特别注意,如规定好输入Excel文件的格式,对中文的处理,Excel文件中每个单元格中是否有空格或其他的一些违法字符,还有对输入路径合法性的检查,文件后缀的判定等等。尽管比较简单,但花了几天时间,凑了点学费还是不错的,O(∩_∩)O哈哈哈~
]]>QString 是不存在中文支持问题的,很多人遇到问题,并不是本身 QString 的问题,而是没有将自己希望的字符串正确赋给QString。
“我是中文”这样写的时候,它是传统的 char 类型的窄字符串,我们需要的只不过是通过某种方式告诉QString 这四个汉字采用的那种编码。而问题一般都出在很多用户对自己当前的编码没太多概念。
在这里介绍一篇对字符编码讲解的很透彻的一篇文章:ASCII、Unicode、GBK和UTF-8字符编码的区别联系
QString的内部是使用的unicode编码,所以本质就是将所有字符转换成unicode编码即可正常显示。
所以当使用这样的代码:1
QString a = "我是中文"
其实等价于1
2const char* s = "我是中文"
QString a = s;
那么当需要从窄字符串char转成Unicode的QString字符串时,你需要告诉Qt你的这个char 是什么编码?GBK、BIG5、Latin-1.
所以关于Qt的中文乱码,Qt4和Qt5有不同的解决方案。
在Qt4中,常用的解决方案是在main.cpp加这几行代码1
2
3
4QTextCodec *codec = QTextCodec::codecForName("system");//获取系统中文编码
QTextCodec::setCodecForLocale(codec);
QTextCodec::setCodecForCStrings(codec);
QTextCodec::setCodecForTr(codec);
这几行代码就是告诉程序你的char* 中到底使用的是什么编码。
而在Qt5中取消了这几个函数,取而代之的是另外的解决方案。
用QTextCodec类中的转换函数1
2
3
4
5
6
7
8
9
10
11std::string utf82gbk(const QString &inStr)
{
QTextCodec *gbk = QTextCodec::codecForName("GB18030");
return gbk->fromUnicode(inStr).data();
}
QString gbk2utf8(const std::string &inStr)
{
QTextCodec *gbk = QTextCodec::codecForName("GB18030");
return gbk->toUnicode(inStr.c_str());
}
由于我最近很差钱,所以digitalocean就是首选了,我选的是最低配的$5一个月的,首次购买点击这个链接注册可以送$10,相当于$5可以用三个月啊,相当划算。
点击注册成功之后,它会让你绑定信用卡或者paypal,绑定成功后就可以购买啦,如下图这样
然后点击Create Droplet,之后就看到类似这样
选旧金山的机房,据说是针对国内用户优化过,速度相比较于其他几个机房要快一些,然后我选的是ubuntu的系统,这个随意吧,自己喜欢哪个就选哪个系统,最后点击创建之后大概需要一分钟
创建成功之后,大概类似这样
然后你的邮箱里面会受到root的密码,这个密码是用来起始登录的,点击Console Access之后,进入系统内部,首次登录,需要修改root密码
修改成功之后就可以在任何地方用ssh登录啦,接下来就是配置VPN了和shadowsocks了,二者选其一即可,关于VPN和shadowsocks的解释再次不赘述,不懂请自行谷歌。
如果是ubuntu系统,可以直接将下面的代码保存为.sh脚本文件,然后只需要修改两个地方
1.第35行修改为自己刚才创建的VPS的ip地址
2.第59行修改为自己的用户名和密码,可以添加多个用户名和密码
1 | #!/bin/sh |
然后保存为pptp.sh,修改权限,运行完成之后配置就完成啦。1
2chmod +x pptp.sh
./pptp.sh
你的VPN账号就是上面第59行设置的用户名和密码啦。
1) 安装shadowsocks服务器1
2apt-get install python-pip
pip install shadowsocks
2) 保存配置文件1
sudo vim /etc/shadowsocks.json
这样会打开一个空白文件,输入下面配置信息1
2
3
4
5
6
7
8
9
10{
"server":"0.0.0.0",
"server_port":8033,
"local_address": "127.0.0.1",
"local_port":1080,
"password":"set-your-password",
"timeout":300,
"method":"aes-256-cfb",
"fast_open": false
}
“server”如果你设置为0.0.0.0后不能连接,你可以设为你的vps ip地址试试。
“server_port”这个是可以自己随意指定一个,但是下面连接的时候会用到。
“local_port”这个也是可以随意指定的,但是不要跟“server_port”一样,下面也会用到
“password”自己设一个密码。
3) 运行shadowsocks 服务1
ssserver -c /etc/shadowsocks.json -d start
如果想关闭的话1
ssserver -c /etc/shadowsocks.json -d stop
4) 下载shadowsocks客户端
http://shadowsocks.org/en/download/clients.html
5) 打开客户端连接
以windows系统为例,类似这样的图
服务器端口号和代理端口号就是上面设置的两个号,服务器ip就是你的VPS的ip地址,密码就是刚才设置的。点确定即可。然后配合一些代理插件即可上网啦,推荐chrome下的SwitchOmega插件,安装后怎么配置使用可以自行搜索啦,在此不叙述。
本文教了大家两种方法去到外面的世界去看看,大家有问题可以在下面留言。在天朝真的是上个网都这么麻烦,更加深了我想肉身翻墙的信念了,也祝愿大家都早日实现肉身翻墙,哈哈哈哈。。
]]>由于我最近很差钱,所以digitalocean就是首选了,我选的是最低配的$5一个月的,首次购买点击这个链接注册可以送$10,相当于$5可以用三个月啊,相当划算。
点击注册成功之后,它会让你绑定信用卡或者paypal,绑定成功后就可以购买啦,如下图这样
然后点击Create Droplet,之后就看到类似这样]]>
全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。
符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:
常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。
每种不同类型的常量类型具有不同的结构,具体的结构本文就先不叙述了,本文着重区分这三个常量池的概念(读者若想深入了解每种常量类型的数据结构可以查看《深入理解java虚拟机》第六章的内容)。
当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?
jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
举个实例来说明一下:
1 | String str1 = "abc"; |
上面程序的首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。
回到上面的那个程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例,当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同,str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值,最后str5在解析的时候就也是指向存在于StringTable中的”def”的引用值,那么这样一分析之后,下面三个打印的值就容易理解了。
全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。
符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:
常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。]]>
今天笔者按照所看的书上的内容和自己的理解来简单的谈一下java的内存分配问题。关于java运行时的数据区域,主要涉及以下几个:
程序计数器:它主要存储的是当期线程所执行的字节码的行号等信息,然后字节码解释器根据计数器中存储的内容来选取下一条需要执行的字节码指。因此,可以看出,每条线程都有一个独立的程序计数器,它是线程私有的。
java虚拟机栈和本地方法栈:这两个我放在一起来说,是因为他们两个的作用非常类似,区别不过是虚拟机栈是为虚拟执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务(就是这个方法是用C++/C来实现的,编译成dll供java调用的)。java虚拟机栈也是线程私有的,与线程的生命周期相同。程序中的每个方法在执行的时候都会创建一个栈帧,里面存放了局部变量表、操作栈、动态连接、方法出口信息,局部变量表里存的是程序中基本类型的数据,局部变量和对象的引用等等。每一个方法从调用到完成的过程,就是相应的栈帧在虚拟机栈中压栈和出栈的过程。
java堆:java堆是被所有线程共享的一块区域,它的作用就是存放几乎所有对象的实例,也就是存放new产生的数据(当然还有一些其他技术可以让new出来的数据存放到栈上或其他地方)。
方法区:它也是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
运行时常量池:在方法区里面又划分了一块区域为运行时常量池,用于存放程序中的一切常量,包含代码中所定义的各种基本类型(如int,long等等)和对象型(如String及数组)的常量值(final)。每个Class文件都会有一个常量池,存放的是常量值的符号引用,当类加载后,会将这部分信息存放到运行时常量池,等到类的解析完成之后,会将符号引用替换成直接引用,与全局字符串池(String pool)中的值保持一致。
下面我们来通过一些具体的例子来说明不同情况下java的内存分配情况:
1 | int a = 3; |
编译器先处理int a =3;首先它会在当前方法的栈帧中的局部变量表中创建一个变量为a的引用,然后查找局部变量表中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b =3;在创建完b的引用变量后,因为在局部变量表中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。
这时,如果再令 a=4;那么编译器会重新搜索局部变量表中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。
我们再看下面这个例子:1
2String str = new String("abc");
String str = "abc";
这两种方法都可以创建一个String对象,第一种方法会创建两个实例对象,一个是在类解析的时候,生成一个实例对象放到堆中,然后字符串池(String pool)中存放该实例的引用,第二个实例对象是在运行的时候用new来动态创建的。而第二种方法是直接在类解析的时候回生成一个实例对象放到堆中,然后字符串池(string pool)中存放该实例的引用。
当我们比较String对象的时候,用equals来比较两个String对象的内容是否相同,用==来比较二者的引用是否指向同一个对象,看代码:1
2
3String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
可以看出str1和str2是指向同一个对象的。这是因为,在类解析的到String str1 = “abc”的时候就会在堆中创建一个”abc”对象,然后在全局字符串池(String pool)中存放这个引用,当解析到str2的时候会先在String pool中查询是否有”abc”这个值的引用中,如果有的话,就直接将这个引用值赋给str2,所以str1和str2指的是同一个对象。1
2
3String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
用new的方式是生成不同的对象,每new一次生成一个。
总结一下就是说:使用诸如String str = “abc”并不能保证一定会创建对象,有可能只是指向一个已经创建好的对象,而通过new()方法是能保证每次会创建一个新对象。
再看下面的一个例子:1
2
3
4
5
6
7
8String s0="kvill";
String s1=new String("kvill");
String s2="kv" + new String("ill");
String s3="kv" + "ill";
System.out.println( s0==s1 );//false
System.out.println( s0==s2 );//false
System.out.println( s1==s2 );//false
System.out.println( s0==s3 );//true
从上面可以看出s0是常量,在类解析的时候就被确定了。而”kv”和”ill”也都是字符串常量,当一个字 符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s3也同样在被解析为一个字符串常量,所以s3也是常量池中” kvill”的一个引用。而使用new String()则不是常量,不能在编译的时候被确定,所以他们有自己的地址空间,在堆中。
我们所说的运行时常量池,不只是包括在编译的时候产生的常量,也可以在运行的时候扩展,用的最多的方法就是String.intern()方法,当一个String实例str调用intern()方法时,Java 查找字符串常量池中 是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在字符串常量池中增加该对象的引用值,看下面示例:1
2
3
4
5
6
7
8
9String s0= "kvill";
String s1=new String("kvill");
String s2=new String("kvill");
System.out.println( s0==s1 ); //false
s1.intern();
s2=s2.intern(); //把字符串常量池中"kvill"的引用值赋给s2
System.out.println( s0==s1); //false
System.out.println( s0==s1.intern() );//true
System.out.println( s0==s2 ); //true
上面例子比较容易懂,s2.intern()返回的是在常量池中”kvill”的引用,所以与s0是相等的。
再来看下面这个例子:1
2
3
4
5
6
7String a = "ab";
String b = "b";
final String c = "b";
String ab = "a" + b;
String ac = "a" + c;
System.out.println((a == ab)); //result = false
System.out.println((a == ac)); //result = true
JVM对于字符串引用,在“+”号连接中,有字符串引用的存在,而引用的值在程序编译器是无法确定的,所以”a”+b的值只有在程序运行期来动态分配并将新的地址赋给ab,所以a!=ab,对于被final修饰的变量,在编译的时候被解析为常量值的变量会将该常量的引用值加入到自己的常量池中,所以此时”a”+c和”a”+”b”的效果是一样的,所以为true。
对于方法的调用,也是无法再编译器确定,如下:1
2
3
4
5
6
7String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println((a == b)); //result = false
private static String getBB() {
return "b";
}
对于字符串引用bb,它只有在程序运行期间调用方法后,将方法的返回值和”a”来动态连接并分配地址为b,所以上面的程序结果为false。
总结
参考链接:http://theopentutorials.com/tutorials/java/strings/string-literal-pool/
]]>今天笔者按照所看的书上的内容和自己的理解来简单的谈一下java的内存分配问题。关于java运行时的数据区域,主要涉及以下几个:
程序计数器:它主要存储的是当期线程所执行的字节码的行号等信息,然后字节码解释器根据计数器中存储的内容来选取下一条需要执行的字节码指。因此,可以看出,每条线程都有一个独立的程序计数器,它是线程私有的。
java虚拟机栈和本地方法栈:这两个我放在一起来说,是因为他们两个的作用非常类似,区别不过是虚拟机栈是为虚拟执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务(就是这个方法是用C++/C来实现的,编译成dll供java调用的)。java虚拟机栈也是线程私有的,与线程的生命周期相同。程序中的每个方法在执行的时候都会创建一个栈帧,里面存放了局部变量表、操作栈、动态连接、方法出口信息,局部变量表里存的是程序中基本类型的数据,局部变量和对象的引用等等。每一个方法从调用到完成的过程,就是相应的栈帧在虚拟机栈中压栈和出栈的过程。
java堆:java堆是被所有线程共享的一块区域,它的作用就是存放几乎所有对象的实例,也就是存放new产生的数据(当然还有一些其他技术可以让new出来的数据存放到栈上或其他地方)。
方法区:它也是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
运行时常量池:在方法区里面又划分了一块区域为运行时常量池,用于存放程序中的一切常量,包含代码中所定义的各种基本类型(如int,long等等)和对象型(如String及数组)的常量值(final)。每个Class文件都会有一个常量池,存放的是常量值的符号引用,当类加载后,会将这部分信息存放到运行时常量池,等到类的解析完成之后,会将符号引用替换成直接引用,与全局字符串池(String pool)中的值保持一致。
]]>_注意:所有的安装路径都尽量不要有空格和中文,不然后面会很麻烦。_
首先安装编译所需要的库,在安装OSGeo4w时,选择advanced install->install from Internet,其他默认,然后到了安装库文件的地方,选择搜索安装下面的这些库:
- Proj >=4.4.x
- GEOS >= 3.0
- Sqlite3 >=3.0.0
- GDAL/OGR >=1.4.x
- Qwt >= 5.0& (< 6.1 with internal QwtPolar)
- expat
- fcgi
- gdal
- grass
- gsl-devel
- iconv
- pyqt4
- qt4-devel
- qwt5-devel-qt4
- sip
- spatialite
- libspatialindex-devel
- python-qscintilla
- expat
安装好库之后安装剩余的其他软件,讲flex和bison的bin路径加到环境变量path中去。
安装好所有的东西之后,打开CMake-gui,首先配置前两个source code和build the binaries,分别是源文件的路径和生成的编译文件的路径,配好之后点击Configure,选择VisualStudio10,也就是vs2010,然后就会出现一片红色报错,那是因为我们还没有配置好库的路径,分别配置好库的路径之后继续点Configure,然后直到最后没有报错出现Configuring done,最终的配置如下图所示:
注意:CMAKE的路径是最后编译后的文件生成路径,直接新建一个空的文件夹就行。把with里面的withbindings勾选去掉,还有一个问题就是expat的库,我安装osgeo4w时,安装expat后找不到他的库,然后我又重新下载编译的,如果你的osgeo4w的库里面有的话,直接添加,如果没有的话就跟我一样,重新下载编译,其他的你找不到的库就留空就行。 最后点击Generate,然后到你的build binaries里面看是不是生成了很多的文件,包含一个vs的工程文件,如下图所示:
打开vs2010,打开qgis2.4.0.sln,然后里面就导入所有的工程,你可以选择生成整个解决方案,那样时间会很长,如果只是需要得到开发库文件的话,只需要编译install工程,但是单独编译这个工程好像不行。
注意 : 1.在编译的过程中可能会提示找不到unistd.h文件,讲flex安装目录下的include文件夹下的unistd.h复制到D:\Program Files\Microsoft Visual Studio 10.0\VC\include(vs的安装目录)里,然后重新编译就可以了。
2.编译过程中可能会报spatialite相关的错误,解决方案是讲osgeo4w/include目录下的spatialite.h文件复制覆盖到osgeo4w/include/spatialite文件加下。重新编译。
最后就可以在CMAKE填的那个路径下面看到生成的所有文件。类似下图所示:
接下来就可以得到完整的开发库了,inlude文件夹下是所有的头文件,lib文件夹下包括四个主要的lib文件,bin文件夹下包括四个主要的dll文件。
注意 :要使项目运行,还要将qgis安装目录(别跟编译目录搞混,在官网可以下载)下的bin文件夹下的所有dll文件和刚才得到的四个dll文件放在一起构成运行的所有需要的dll文件,在运行的过程中,还有可能提示缺少libxml2.dll,直接在网上下载或者安装osgeo4w时,勾选libxml2,到bin目录下拷贝一份到项目的dll文件目录中。
至此,所有编译和配置环境相关的工作就完成了,大家在编译或者配置的过程中也许还会碰到其他的问题,欢迎与我交流。
]]>_注意:所有的安装路径都尽量不要有空格和中文,不然后面会很麻烦。_
首先安装编译所需要的库,在安装OSGeo4w时,选择advanced install->install from Internet,其他默认,然后到了安装库文件的地方,选择搜索安装下面的这些库:
- Proj >=4.4.x
- GEOS >= 3.0
- Sqlite3 >=3.0.0
- GDAL/OGR >=1.4.x
- Qwt >= 5.0& (< 6.1 with internal QwtPolar)
- expat
- fcgi
- gdal
- grass
- gsl-devel
- iconv
- pyqt4
- qt4-devel
- qwt5-devel-qt4
- sip
- spatialite
- libspatialindex-devel
- python-qscintilla
- expat
安装好库之后安装剩余的其他软件,讲flex和bison的bin路径加到环境变量path中去。]]>
我们通常都习惯寻找一个事物在一段时间里的变化模式(规律)。这些模式发生在很多领域,比如计算机中的指令序列,句子中的词语顺序和口语单词中的音素序列等等,事实上任何领域中的一系列事件都有可能产生有用的模式。
考虑一个简单的例子,有人试图通过一片海藻推断天气——民间传说告诉我们‘湿透的’海藻意味着潮湿阴雨,而‘干燥的’海藻则意味着阳光灿烂。如果它处于一个中间状态(‘有湿气’),我们就无法确定天气如何。然而,天气的状态并没有受限于海藻的状态,所以我们可以在观察的基础上预测天气是雨天或晴天的可能性。另一个有用的线索是前一天的天气状态(或者,至少是它的可能状态)——通过综合昨天的天气及相应观察到的海藻状态,我们有可能更好的预测今天的天气。
首先,我们将介绍产生概率模式的系统,如晴天及雨天间的天气波动。
然后,我们将会看到这样一个系统,我们希望预测的状态并不是观察到的——其底层系统是隐藏的。在上面的例子中,观察到的序列将是海藻而隐藏的系统将是实际的天气。
最后,我们会利用已经建立的模型解决一些实际的问题。对于上述例子,我们想知道:
确定性模式(Deterministic Patterns)
考虑一套交通信号灯,灯的颜色变化序列依次是红色-红色/黄色-绿色-黄色-红色。这个序列可以作为一个状态机器,交通信号灯的不同状态都紧跟着上一个状态。
注意每一个状态都是唯一的依赖于前一个状态,所以,如果交通灯为绿色,那么下一个颜色状态将始终是黄色——也就是说,该系统是确定性的。确定性系统相对比较容易理解和分析,因为状态间的转移是完全已知的。
非确定性模式(Non-deterministic patterns)
为了使天气那个例子更符合实际,加入第三个状态——多云。与交通信号灯例子不同,我们并不期望这三个天气状态之间的变化是确定性的,但是我们依然希望对这个系统建模以便生成一个天气变化模式(规律)。
一种做法是假设模型的当前状态仅仅依赖于前面的几个状态,这被称为马尔科夫假设,它极大地简化了问题。显然,这可能是一种粗糙的假设,并且因此可能将一些非常重要的信息丢失。
当考虑天气问题时,马尔科夫假设假定今天的天气只能通过过去几天已知的天气情况进行预测——而对于其他因素,譬如风力、气压等则没有考虑。在这个例子以及其他相似的例子中,这样的假设显然是不现实的。然而,由于这样经过简化的系统可以用来分析,我们常常接受这样的知识假设,虽然它产生的某些信息不完全准确。
一个马尔科夫过程是状态间的转移仅依赖于前n个状态的过程。这个过程被称之为n阶马尔科夫模型,其中n是影响下一个状态选择的(前)n个状态。最简单的马尔科夫过程是一阶模型,它的状态选择仅与前一个状态有关。这里要注意它与确定性系统并不相同,因为下一个状态的选择由相应的概率决定,并不是确定性的。
下图是天气例子中状态间所有可能的一阶状态转移情况:
对于有M个状态的一阶马尔科夫模型,共有M^2个状态转移,因为任何一个状态都有可能是所有状态的下一个转移状态。每一个状态转移都有一个概率值,称为状态转移概率——这是从一个状态转移到另一个状态的概率。所有的M^2个概率可以用一个状态转移矩阵表示。注意这些概率并不随时间变化而不同——这是一个非常重要(但常常不符合实际)的假设。
下面的状态转移矩阵显示的是天气例子中可能的状态转移概率:
-也就是说,如果昨天是晴天,那么今天是晴天的概率为0.5,是多云的概率为0.375。注意,每一行的概率之和为1。
要初始化这样一个系统,我们需要确定起始日天气的(或可能的)情况,定义其为一个初始概率向量,称为pi向量。
-也就是说,第一天为晴天的概率为1。
现在我们定义一个一阶马尔科夫过程如下:
状态转移矩阵:给定前一天天气情况下的当前天气概率。
任何一个可以用这种方式描述的系统都是一个马尔科夫过程。
马尔科夫过程的局限性
在某些情况下,我们希望找到的模式用马尔科夫过程描述还显得不充分。回顾一下天气那个例子,一个隐士也许不能够直接获取到天气的观察情况,但是他有一些水藻。民间传说告诉我们水藻的状态与天气状态有一定的概率关系——天气和水藻的状态是紧密相关的。在这个例子中我们有两组状态,观察的状态(水藻的状态)和隐藏的状态(天气的状态)。我们希望为隐士设计一种算法,在不能够直接观察天气的情况下,通过水藻和马尔科夫假设来预测天气。
一个更实际的问题是语音识别,我们听到的声音是来自于声带、喉咙大小、舌头位置以及其他一些东西的组合结果。所有这些因素相互作用产生一个单词的声音,一套语音识别系统检测的声音就是来自于个人发音时身体内部物理变化所引起的不断改变的声音。
一些语音识别装置工作的原理是将内部的语音产出看作是隐藏的状态,而将声音结果作为一系列观察的状态,这些由语音过程生成并且最好的近似了实际(隐藏)的状态。在这两个例子中,需要着重指出的是,隐藏状态的数目与观察状态的数目可以是不同的。一个包含三个状态的天气系统(晴天、多云、雨天)中,可以观察到4个等级的海藻湿润情况(干、稍干、潮湿、湿润);纯粹的语音可以由80个音素描述,而身体的发音系统会产生出不同数目的声音,或者比80多,或者比80少。
在这种情况下,观察到的状态序列与隐藏过程有一定的概率关系。我们使用隐马尔科夫模型对这样的过程建模,这个模型包含了一个底层隐藏的随时间改变的马尔科夫过程,以及一个与隐藏状态某种程度相关的可观察到的状态集合。
隐马尔科夫模型(Hidden Markov Models)
下图显示的是天气例子中的隐藏状态和观察状态。假设隐藏状态(实际的天气)由一个简单的一阶马尔科夫过程描述,那么它们之间都相互连接。
隐藏状态和观察状态之间的连接表示:在给定的马尔科夫过程中,一个特定的隐藏状态生成特定的观察状态的概率。这很清晰的表示了‘进入’一个观察状态的所有概率之和为1,在上面这个例子中就是Pr(Obs|Sun), Pr(Obs|Cloud) 及 Pr(Obs|Rain)之和。(对这句话我有点疑惑?)
除了定义了马尔科夫过程的概率关系,我们还有另一个矩阵,定义为混淆矩阵(confusion matrix),它包含了给定一个隐藏状态后得到的观察状态的概率。对于天气例子,混淆矩阵是:
注意矩阵的每一行之和是1。
总结(Summary)
我们已经看到在一些过程中一个观察序列与一个底层马尔科夫过程是概率相关的。在这些例子中,观察状态的数目可以和隐藏状态的数码不同。
我们使用一个隐马尔科夫模型(HMM)对这些例子建模。这个模型包含两组状态集合和三组概率集合:
参考文章:http://www.52nlp.cn/hmm-learn-best-practices-one-introduction
]]>我们通常都习惯寻找一个事物在一段时间里的变化模式(规律)。这些模式发生在很多领域,比如计算机中的指令序列,句子中的词语顺序和口语单词中的音素序列等等,事实上任何领域中的一系列事件都有可能产生有用的模式。
考虑一个简单的例子,有人试图通过一片海藻推断天气——民间传说告诉我们‘湿透的’海藻意味着潮湿阴雨,而‘干燥的’海藻则意味着阳光灿烂。如果它处于一个中间状态(‘有湿气’),我们就无法确定天气如何。然而,天气的状态并没有受限于海藻的状态,所以我们可以在观察的基础上预测天气是雨天或晴天的可能性。另一个有用的线索是前一天的天气状态(或者,至少是它的可能状态)——通过综合昨天的天气及相应观察到的海藻状态,我们有可能更好的预测今天的天气。
首先,我们将介绍产生概率模式的系统,如晴天及雨天间的天气波动。
然后,我们将会看到这样一个系统,我们希望预测的状态并不是观察到的——其底层系统是隐藏的。在上面的例子中,观察到的序列将是海藻而隐藏的系统将是实际的天气。
最后,我们会利用已经建立的模型解决一些实际的问题。对于上述例子,我们想知道:
中国人名识别的主要困难有:
针对这个问题目前存在的主要解决方案有:规则方法,统计方法以及规则和统计相结合的方法。规则方法主要利用了姓氏分类和名字组成的限制性,如名字的组成一般不会超过四个字。但是由于它需要“单点激发”,即它需要扫描到姓氏、职衔、称呼后才开始识别,所以往往无法识别不具有明显特征的人名,例如“有名无姓”的情况。统计方法主要是针对姓名语料库来训练某个字作为姓名组成部分的概率,并利用它们来计算某个字段作为姓名的概率值,其中概率值大于某一阈值的字段识别为中国人名。统计方法对语料库的规模要求很高。所以,在这种情况下,提出了基于角色标注的中文人名自动识别算法。该方法的大致思路是用隐马尔科夫模型(HMM)在分词结果上标注人名构成的角色,然后在标注出的角色序列基础上根据各个不同的角色,进行最长模式匹配,最终识别出人名。下面分别来说明这几个步骤:
角色标注相当于是在已经粗分好的结果上标注出各个词的词性,只不过此时的词性不再是动词、名词、形容词等,而是针对它们和人名的关系划分为人名的内部组成、上下文、无关词。具体的角色分类如表1 所示。
表1 中国人名的构成角色
根据该表,对切分结果“馆/内/陈列/周/恩/来/和/邓/颖/超生/前/使用/过/的/物品”进行角色标注,其结果为:“馆/A 内/A 陈列/K 周/B 恩/C 来/D 和/M邓/B 颖/C 超生/V 前/A 使用/A 过/A 的/A 物品A”(“周”、“邓”是姓氏标为B,“恩”、“颖”是双名首字标为C,“来”是双名末字标为D,“和”是两个人名之间的连接词标为M,“超”字和后面的“生”字因为组成了一个词语所以标为V)。
那么如何自动进行角色标注呢?
这里我们采用了viterbi 算法,也就是说从所有可能的标注结果中选出概率最大者作为最终标注结果。具体推导过程如下:
假定 W 是 Token 序列(即粗分后得到的序列)
T 是 W 某个可能的角色标注序列
T#是最终的标注结果,即概率最大的序列。则有:
W=(w1,w2,……,wm)
T=(t1,t2,……,tm),m>0
T#=arg max P(T|W)…………E1
根据贝叶斯公式有,P(T|W)=P(W|T)P(T)/P(W)…………E2
在基于 N-最短路径的统计粗分模型分析中可知,P(W)是一个常数,所以得
到:T#=arg max P(W|T)P(T)…………E3
如果把 wi 视为观察值,把角色 ti 视为状态值(t0 为初始状态)。那么可以把
这个问题看做一个隐马尔科夫模型(HMM)的问题:即已知观察观察序列 W,求状态序列T。对状态序列T 做一阶马尔科夫假设:ti 出现的概率只与ti-1 有关,
p(ti|t1,t2,……,ti-1)=p(ti|ti-1);假设当前的输出只与当前的状态有关,p(w1,w2,……wm|t1,t2,……,tm)=p(w1|t1)p(w2|t2)……p(wm|tm)。
综上所述:
T#=arg max P(W|T)P(T)
=arg max p(w<sub>1</sub>|t<sub>1</sub>)p(w<sub>2</sub>|t<sub>2</sub>)……p(w<sub>m</sub>|t<sub>m</sub>)p(t<sub>1</sub>|t<sub>0</sub>)p(t<sub>2</sub>|t<sub>1</sub>)……p(t<sub>m</sub>|t<sub>m-1)</sub>
=arg max _p_(_wi_ | _ti_) _p_(_ti_ | _ti_ 1) …………E4
为方便计算对 E4 取负对数:
T#=arg min( _i__m_1{ln _p_(_wi_ | _ti_) ln _p_(_ti_ | _ti_ 1)}) …………E5
用 viterbi 算法求解 E5 后,T#就求得了 从上述推导过程知,p(wi|ti)、p(ti|ti-1)是关键参数(p(wi|ti)是在给定角色 ti 的条件下,Token 是 wi 的概率;p(ti|ti-1)是角色 ti-1 到 ti 的转移概率),它们可以根据大数 定理求得: p(wi|ti)≈c(wi,ti)/c(ti);
c(wi,ti)是 wi 作为角色 ti 出现次数,c(ti)是角色 ti 出现的次数。
i>1 时,p(ti|ti-1)≈c(ti-1,ti)/c(ti-1)
c(ti-1,ti)是角色 ti-1 的下一个角色是 ti 出现的次数。
c(wi,ti)、c(ti)、c(ti-1,ti)都可以通过已经切分且标注好的熟语料库学习得到。
按U 的组成方式对其进行分裂(假设U 由pf 两个字构成):若f 为姓,则分裂为KB;若f 为双名首字,则分裂为KC;若f 为单名则分裂为KE。按V 的组成方式对其进行分裂(假设V 有tn 分裂为DL):若t 为双名末字,则分裂为DL;若t 为单名,则分裂为EL
使用到的人名识别模式集为:{BBCD,BBE,BBZ,BCD,BE,BG,BXD,BZ,CD,FB,Y,XD}。一旦匹配到其中一个最长的模式串,则对应的Token 片段就识别为中国人民。例如:“馆/内/陈列/周/恩/来/和/邓/颖/超生/前/使用/过/的/物品”经过viterbi算法计算后,对应的T#为“AAKBCDMBCDLAAAAAA”。所以模式最大匹配后,识别得到的人名是:“周恩来”和“邓颖超”。
机构名的识别和人名的识别类似,不同之处在于,机构名的构成更复杂且规律性更少(例如大多数机构有简称,机构名长度不定等)。从组成方式来看,完整的机构名可以分为前段和后端两部分。如“北京电视台”中“北京”为前段,“电视台”为后段。且机构名的上下文大多是一些连词、动词或者表示职务的名词等。如“主席”、“经理”。所以,根据这些特点,得到角色表如表二所示:
表二机构名角色表
例如对切分结果:“在/1998 年/来临/之际/,/通过/中央/人民/广播/电台/向/全国/各族/人民/致以/诚挚/的/问候/和/良好/的/祝愿/!/”进行角色标注,得到的结果为:“在/Z1998 年/Z 来临/Z 之际/Z,/Z 通过/A 中央/I 人民/I 广播/C 电台/D向/Z 全国/Z 各族/Z 人民/Z 致以/Z 诚挚/Z 的/Z 问候/Z 和/Z 良好/Z 的/Z 祝愿/Z!/Z”。与人名识别类似,机构名通过viterbi 算法自动标注,再通过字符串比较找出满足[CFGHIJ]D 的子串,则机构名得以识别。
参考资料:
1、《基于角色标注的中国人名自动识别研究》
http://www.ictclas.org/ictclas_files.html
2、《基于角色标注的中文机构名识别》
http://www.ictclas.org/ictclas_files.html
3、有关HMM 的介绍:
http://blog.csdn.net/likelet/article/details/7056068
4、Viterbi 算法:
http://zh.wikipedia.org/wiki/%E7%BB%B4%E7%89%B9%E6%AF%94%E7
%AE%97%E6%B3%95
中国人名识别的主要困难有:
针对这个问题目前存在的主要解决方案有:规则方法,统计方法以及规则和统计相结合的方法。规则方法主要利用了姓氏分类和名字组成的限制性,如名字的组成一般不会超过四个字。但是由于它需要“单点激发”,即它需要扫描到姓氏、职衔、称呼后才开始识别,所以往往无法识别不具有明显特征的人名,例如“有名无姓”的情况。统计方法主要是针对姓名语料库来训练某个字作为姓名组成部分的概率,并利用它们来计算某个字段作为姓名的概率值,其中概率值大于某一阈值的字段识别为中国人名。统计方法对语料库的规模要求很高。所以,在这种情况下,提出了基于角色标注的中文人名自动识别算法。该方法的大致思路是用隐马尔科夫模型(HMM)在分词结果上标注人名构成的角色,然后在标注出的角色序列基础上根据各个不同的角色,进行最长模式匹配,最终识别出人名。下面分别来说明这几个步骤:
]]>
N-最短路径的原理与1-最短路径的相同,只不过求解过程要稍微复杂一点。让我们仍然以上篇文章中的实例来看看如何求解N-最短路径。
我们沿用上篇文章中的例子,各权重已经标出:
(图一)
根据有向图可以建立如下所示的二维表:
仍然像1-最短路径一样,求出各个节点的PreNode链表,这里我们以2-最短路径为例:
分别计算源点到各个节点的所有最短路径和次最短路径,我们可以得到如下所示的PreNode链表:
(图三)
说明:在上图中,可以看到源点(0号节点)到A,B,C的路径长度分别只有一条,但是到D的路径长度有两个,一个是3,一个是4,此时在“最短路”处(index=0)记录长度为3时的PreNode,在“次短路”处(index=1)处记录长度为4时的PreNode,依此类推。
值得注意的是,此时用来记录PreNode的坐标已经由前文求“1-最短路径”时的一个数(ParentNode值)变为2个数(ParentNode值以及index值)。
如图三所示,到达6号“末”结点的次短路径由两个ParentNode,一个是index=0中的4号结点,一个是index=1的5号结点,它们都使得总路径长度为6。
求解N-最短路径的方法与1-最短路径是一样的,也是借助堆栈完成的。只不过根据index取值的不同,分多次出栈和入栈。
根据求解方法,我们求得了2-最短路径,路径长度有两种,分别长度为5和6,而路径总共有6条,如下:
最短路径:
次短路径
实现算法如下:
1 | int CNShortPath::ShortPath() |
说明:m_nValueKind即为N-最短路径中N的取值,在这即N=2.。
]]>N-最短路径的原理与1-最短路径的相同,只不过求解过程要稍微复杂一点。让我们仍然以上篇文章中的实例来看看如何求解N-最短路径。
我们沿用上篇文章中的例子,各权重已经标出:
(图一)
根据有向图可以建立如下所示的二维表:
(图二)
]]>
首先我们要知道,N-最短路径算法中的N指的是什么?
_N指的是从源点到终点的前N条最短路径长度的若干条路径,这里有个地方要注意的是N指的是前N条最短路径长度。_例如:求2-最短路径,从源点到终点的最短路径长度为5,次短路径长度为6,但是包含最短路径长度的不一定就只有一条路径,譬如可能有两条路径A,B都是路径长度为5,又有三条路径C,D,E的长度为6,那么求2-最短路径即求A,B,C,D,E这五条路径。这个问题清楚之后,我们就开始下面的问题。
在了解N-最短路径之前,首先,我们来看看1-最短路径算法。顾名思义,1-最短路径即求的是最短路径,下面我们来看看1-最短路径是如何实现的:
我们用如下的有向图来举例说明,各权重已标明:
(图一)
根据有向图可以建立如下所示的二维表:
(图二)
要求出源点到该点最短路径的PreNode链表,可根据Dijkstra算法求出源点到每个点的最短路径及其PreNode,求解过程略,可得到如下图所示的PreNode链表:
(图三)
说明:该点有多个PreNode,是因为源点到该点的最短路径可能不止一条,因此会有多个PreNode,例如:从源点到C的最短路径为3,但是有两条路径可取到最短路径,所以C的PreNode有两个,一个是A,一个是B。
(图四)
算法思想如下:
(图五)
再往下,0、1、2都被弹出堆栈,3被弹出堆栈后,由于它对应的6号元素PreNode队列记录指针仍然可以下移,因此将5压入堆栈并依次将其PreNode入栈,直到0被入栈。此时输出第3条最短路径:0, 1, 2, 4, 5, 6。入下图:
(图六)
输出完成后,紧接着又是出栈,此时已经没有任何堆栈元素对应的PreNode队列指针可以下移,于是堆栈中的最后一个元素6也被弹出堆栈,此时输出工作完全结束。我们得到了3条最短路径,分别是:
首先我们要知道,N-最短路径算法中的N指的是什么?
_N指的是从源点到终点的前N条最短路径长度的若干条路径,这里有个地方要注意的是N指的是前N条最短路径长度。_例如:求2-最短路径,从源点到终点的最短路径长度为5,次短路径长度为6,但是包含最短路径长度的不一定就只有一条路径,譬如可能有两条路径A,B都是路径长度为5,又有三条路径C,D,E的长度为6,那么求2-最短路径即求A,B,C,D,E这五条路径。这个问题清楚之后,我们就开始下面的问题。
在了解N-最短路径之前,首先,我们来看看1-最短路径算法。顾名思义,1-最短路径即求的是最短路径,下面我们来看看1-最短路径是如何实现的:
我们用如下的有向图来举例说明,各权重已标明:
(图一)
根据有向图可以建立如下所示的二维表:
(图二)
二维表内取值即为权重。
]]>
中文分词的算法大致可分为三大类:基于字符串匹配的分词方法、基于理解的分词方法和基于统计的分词方法。而基于字符串匹配的方法中,常用的方法主要有:最大匹配(包括向前、向后以及前后相结合)、最短路径方法(切分出来的词数最少)、全切分方法(列出所有可能的分词结果)、以及最大概率方法(训练一个一元语言模型,通过计算,得到一个概率最大的分词结果)。下面针对各自的优缺点,对比分析如下:
N-最短路径方法相对的不足就是粗分结果不唯一,后续过程需要处理多个粗分结果。但是,对于预处理过程来讲,粗分结果的高召回率至关重要。因为低召回率就意味着没有办法再作后续的补救措施。预处理一旦出错,后续处理只能是一错再错,基本上得不到正确的最终结果。而少量的粗分结果对后续过程的运行效率影响不会太大,后续处理可以进一步优选排错,如词性标注、句法分析等。
而本项目所采用的NLPIR分词系统就是采用N-最短路径方法来实现中文词语的粗分。在下篇文章《N-最短路径中文词语粗分算法研究》中,再来着重讨论N-最短路径算法的原理实现。
]]>中文分词的算法大致可分为三大类:基于字符串匹配的分词方法、基于理解的分词方法和基于统计的分词方法。而基于字符串匹配的方法中,常用的方法主要有:最大匹配(包括向前、向后以及前后相结合)、最短路径方法(切分出来的词数最少)、全切分方法(列出所有可能的分词结果)、以及最大概率方法(训练一个一元语言模型,通过计算,得到一个概率最大的分词结果)。下面针对各自的优缺点,对比分析如下: