advse/2.tex

345 lines
21 KiB
TeX
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

%\documentclass[a4paper,12pt,UTF8,titlepage]{ctexart}
\documentclass[12pt,a4paper,titlepage]{article}
\usepackage{xltxtra,fontspec,xunicode}
\usepackage[slantfont,boldfont]{xeCJK}
\setmainfont{STSong} % 设置文档默认字体
\usepackage{setspace}%使用间距宏包
\usepackage{indentfirst}
\setlength{\parindent}{2em}
\usepackage{titlesec}
\titleformat*{\section}{\centering\huge\bfseries}
%页边距
\usepackage{geometry}
\geometry{left=2.0cm,right=2.0cm,top=2.5cm,bottom=2.5cm}
%页眉
\usepackage{fancyhdr}
\pagestyle{fancy}
\lhead{李志星 15060025 }
\chead{Spark MLlib应用报告}
\rhead{\leftmark}
%文档信息/同时也用于生成报告封面
\author{李志星\\ 15060025}
\date{2016年6月20日}
\title{\Huge 基于Spark MLlib的手写数字识别\newline \Large Logistic Regression的应用}
\usepackage{graphicx}
\usepackage{subfigure}
\DeclareGraphicsExtensions{.eps,.ps,.jpg,.bmp,.gif,.png}
\usepackage{pythonhighlight}
% "define" Scala
\usepackage{listings}
\lstdefinelanguage{scala}{
morekeywords={abstract,case,catch,class,def,%
do,else,extends,false,final,finally,%
for,if,implicit,import,match,mixin,%
new,null,object,override,package,%
private,protected,requires,return,sealed,%
super,this,throw,trait,true,try,%
type,val,var,while,with,yield},
otherkeywords={=>,<-,<\%,<:,>:,\#,@},
sensitive=true,
morecomment=[l]{//},
morecomment=[n]{},
morestring=[b]",
morestring=[b]',
morestring=[b]"""
}
\usepackage{color}
\definecolor{dkgreen}{rgb}{0,0.6,0}
\definecolor{gray}{rgb}{0.5,0.5,0.5}
\definecolor{mauve}{rgb}{0.58,0,0.82}
% Default settings for code listings
\lstset{frame=tb,
language=scala,
aboveskip=3mm,
belowskip=3mm,
showstringspaces=false,
columns=flexible,
basicstyle={\small\ttfamily},
numbers=none,
numberstyle=\tiny\color{gray},
keywordstyle=\color{blue},
commentstyle=\color{dkgreen},
stringstyle=\color{mauve},
frame=single,
breaklines=true,
breakatwhitespace=true
tabsize=3
}
\begin{document}
\begin{spacing}{1.5}
\maketitle
\section{实验内容}
\subsection{手写识别}
手写识别Handwriting recognition是计算机在纸、照片、触摸屏或其他设备中接收并识别人手写的文字等信息的技术主要应用于光学字符识别OCR。手写识别系统能够用来识别汉字、英语、数字等字符。不过本报告的重点不在手写识别而在于理解MLlib中的机器学习算法因此以识别数字为例。识别数字09是个十类别问题分类算法中最常用的场景是二分类如果要用二分类解决这个问题要采用one-against-one和one-against-all等做法进行处理虽然复杂些但是基本原理是一样的。因此本报告把主要关注点放在对MLlib算法的研究只利用0和1的样本从而解决二分类问题。
\subsection{Logistic Regression}
在Zaharia提出Spark的论文中他使用了Logistic Regression来作为一个实例来展示介绍Spark因此在本报告中我也利用Logistic Regression来解决手写识别问题。Logistic Regression是比较常用的机器学习方法用于估计某种事物的可能性广告预测、用户行为预测和疾病预测等都经常用到它。它可以用来做回归也可以用来分类并且主要是二分类在分类的时候Logistic Regression不仅能分别出一个样本属于哪个类还能计算出这个样本属于某个类类的可能性是多少。其实在本质上Logistic Regression是个线性回归它只不过是在特征到结果的映射过程中多加了一个二值函数g映射即先把样本的特征线性求和接着把该连续值映射到0和1上。其大致的思路如下图所示
\begin{figure}[h!]
\centering
\includegraphics[width=8cm]{LG_arch.png}
\caption{Logistic Regression原理示意图}
\label{LG_arch}
\end{figure}
这里w是模型参数也就是回归系数g是sigmoid函数这个函数实际上是由对数几率变换的来的。在对Logistic Regression求解的时候我见过最多的介绍的都是随机梯度下降SGD。梯度下降是利用一阶的梯度信息找到函数局部最优解的一种方法也是机器学习里面最简单最常用的一种优化方法。它的思想很简单要找最小值只需要每一步都往下走也就是每一步都可以让代价函数小一点然后不断的走那肯定能走到最小值的地方需要更快的到达最小值就要每一步都找下坡最快的地方而这个下坡最快的方向就是梯度的负方向了。梯度下降算法在每次更新回归系数的时候都需要遍历整个数据集计算整个数据集的回归误差该方法对小数据集尚可。但当遇到有数十亿样本和成千上万的特征时就有点力不从心了它的计算复杂度太高。改进的方法是一次仅用一个样本点的回归误差来更新回归系数这个方法叫就是随机梯度下降算法。
\subsection{MLlib算法主要机制}
在MLlib中有许多的概念其中对理解其算法比较重要的有DataFramePipelineTransfromer和Estimator。
\begin{itemize}
\item DataFrameMLlib使用SaprkSQL中DataFrame来操作数据集是最近才添加的API。在MLLib模块中有两个包都可以用来调用机器学习算法mllib和ml。mllibRDD操作而ml基于DataFrameml是官方推荐使用的。通过DataFrame可以操作各种各样的数据文本、图像和结构化数据等。DataFrame以命名列的方式组织的分布式数据集 等同于关系型数据库中的一个表和R/Python中的DataFrame类似不过进行了很多的优化。
\item Pipeline在MLlib中有一个很关键的概念Pipeline。在利用解决机器学习问题时经常要用对数据进行一系列的处理MLlib用Pipeline来表示这样的工作流在Pipeline中,包含一组以一定顺序执行的 PipelineStage( Transformer和Estimator)。
\item TransformerTransformer是对特征转换和学习得到的模型的抽象每一个Transfromer都要实现transform()方法它把一个DataFrame处理后得到另一个DataFrame,一般来说新的DataFrame比原来的DataFrame要多一些列。
\item EstimatorEstimator是对一些机器学习算法或者其他的数据处理算法的抽象每一个Estimator都有一个方法fit(),它以DataFrame为参数返回一个模型也就是Transformer比如在MLlib中 LogisticRegression 就是一个Estimator而LogisticRegressionModel就是一个Transformer。
\end{itemize}
一个比较简单形象的例子就是文本文档的处理,如下图*所示,上面一行表示了一个包含3个stage的Pipeline前两个Tokenizer和HashingTF是Transformer,第三个LogisticRegression是一个Estimer。下面一行是一个流经pipeline的数据流,圆柱体代表了DataFrame.当调用pipeline的fit方法时 Tokenizer.transform() 把原始的Raw text切分成单词然后把这些单词添加到原DataFrame中形成新的DataFrame从而让HashingTF.transfore()进行处理他把单词列转化成特征矩阵又添加到新的DataFrame中最后通过 LogisticRegression.fit()方法产生LogisticRegressionModel。生成的模型可以用于后续的测试等。
\begin{figure}[h!]
\centering
\includegraphics[width=16cm]{pipeline.jpg}
\caption{MLlib pipeline}
\label{pipeline}
\end{figure}
\section{实验过程}
\subsection{数据集}
本报告用到的数据集是MNIST的手写识别数据集原始数据集分为多个文件分别是训练数据和测试数据的特征值和类标号。这些文件存储的都是二进制格式处理起来比较不方便因此我在网上又找了一些经过别人处理过了的版本它把数据转换成txt文件一个文本文件对应一个样本文件名表示了类标号和序号文件里面包含一个32*32的0/1矩阵矩阵中每一个点可以看成是手写图像中对应的一个像素点如下图*。训练数据和测试数据中分别有400个样本左右虽然样本量有些小但是对于体验一下Spark机器学习算法的还是可以的。
\begin{figure}[h!]
\centering
\subfigure[数字0对应的一个样本]{
\label{0} %% label for first subfigure
\includegraphics[width=1.0in]{data_0.jpg}}
\hspace{0.2in}
\subfigure[数字1对应的一个样本]{
\label{1} %% label for second subfigure
\includegraphics[width=1.0in]{data_1.jpg}}
\caption{样本示例}
\label{0_1} %% label for entire figure
\end{figure}
\subsection{代码}
如前言中所述MLlib中有两个用于机器学习的包mllib和ml根据应用趋势和其官方网站的建议我采用了ml。Spark本身使用Scala语言编写的但是它同样为Java、Python和R提供了几乎一致的接口在本次报告中我使用的python。代码解释如下
\subsubsection{导入依赖}
此段代码导入需要用到的包包括数据处理的Vectors、算法训练的 LogisticRegression、算法评估BinaryClassificationEvaluator以及其他的一些用于和Spark操作的包。
\begin{python}
from pyspark import SparkContext
from pyspark.sql import SQLContext
from pyspark.ml.classification import LogisticRegression
from pyspark.mllib.linalg import Vectors
from os import listdir
from pyspark.ml.evaluation import BinaryClassificationEvaluator
\end{python}
\subsubsection{初始化环境}
SparkContext是在写Spark程序时入口用来连接Spark并进行后续的操作一般还会结合SparkConf对象来设置对Saprk集群的配置。这里我们用默认的设置即可。
SQLContext用来创建DataFrame。
\begin{python}
sc = SparkContext(appName="PythonlogExample")
sqlContext = SQLContext(sc)
\end{python}
\subsubsection{加载数据}
用于训练的DataFrame中应该包含两列特征向量和类别。其中类别是数字1或者0.特征向量就是把32*32的矩阵转换成一个1024维的向量即可。load\_data函数接受一个表示训练样本的所在的目录的参数遍历该目录下所有的文件也就是样本从样本的名字解析出它的类别是0还是1从文件内容中读取特征向量。然后生成DataFrame数据并返回。
\begin{python}
def load_data(data_folder):
file_list=listdir(data_folder)
file_num=len(file_list)
datas = list()
file_num=len(file_list)
datas = list()
for i in range(file_num):
filename=file_list[i]
fr=open('%s/%s' %(data_folder,filename))
data_in_line = list()
for j in range(32):
line_str=fr.readline()
for k in range(32):
data_in_line.append(int(line_str[k]))
label = filename.split('.')[0].split("_")[0]
datas.append((float(label),Vectors.dense(data_in_line)))
return sqlContext.createDataFrame(datas,["label","features"])
\end{python}
\subsubsection{模型训练}
在加载完训练数据后即可用LogisticRegression来对其进行训练。新建LogisticRegression对象时可以指定一些参数我在这里指定了最大迭代数和正则化参数。调用LogisticRegression的fit函数即可生成相应的LogisticRegressionModel。
\begin{python}
train_df = load_data("train")
lr = LogisticRegression(maxIter=10)
lrModel = lr.fit(train_df)
\end{python}
\subsubsection{模型评估}
利用测试数据对训练得到的模型进行评估BinaryClassificationEvaluator用于评估二分类结果我最后利用其提供的两个指标areaUnderROC和areaUnderPR中的areaUnderROC评价了一下该模型。areaUnderROC是ROC曲线右下角部分占正方形格子的面积比例该值越大说明分类的效果越好。
\begin{python}
test_df = load_data("test")
predictions = lrModel.transform(test_df)
evaluator = BinaryClassificationEvaluator(labelCol="label", rawPredictionCol="rawPrediction", metricName="areaUnderPR")
accuracy=evaluator.evaluate(predictions)
print("Test Error = %g " % (1.0 - accuracy))
\end{python}
\section{实验结果}
在本次试验中我分别设置最大迭代次数为0,5,10来观察算法的训练效果。利用训练得到模型在测试数据上预测得到DataFrame predictions然后调用predictions的show()方法该方法会打印出前20个结果一共五列分别是label,features,rawPrediction,probability和prediction。label和features这两个列是加载的测试数据的类标号和特征向量。rawPrediction,probability和prediction是利用分别对训练数据训练以及模型对测试数据进行预测得到的我查了一下相关文档rawPrediction和probability都是一个向量其维数就是类标号的个数rawPrediction字面上的意思是原始的预测直观的讲就是每个类的置信度confidence并且这个向量中的所有元素相加得到的和是0,probability是在给定rawPrediction的条件概率该样本属于每个类的可能性其计算方法根据所采用的分类算法而不同。在Logistic Regression中是这样计算的 1/(1+exp(-rawPrediction)。prediction是最后对样本的预测它是对rawPrediction利用argmax函数得到的也就是取对应rawPrediction最大的那个类。
\begin{itemize}
\item maxIter=0时算法没有进行迭代因为测试数据中有0和1的样本数是一样的因此它会设置每个类标号的概率为0.5算法把每个样本都预测为0这样就有一半的测试样本是错误的最后的areaUnderROC为0.5。\newline
\begin{figure}[h!]
\centering
\subfigure{
\label{0} %% label for first subfigure
\includegraphics[width=10cm]{show_0.jpg}}
\hspace{0.2in}
\subfigure{
\label{1} %% label for second subfigure
\includegraphics[width=8cm]{areaUnderROC_0.jpg}}
\caption{maxIter=0时预测值和areaUnderROC}
\label{0_1} %% label for entire figure
\end{figure}
\item maxIter=5时areaUnderROC为0.9979。\newline
\begin{figure}[h!]
\centering
\subfigure{
\label{2} %% label for first subfigure
\includegraphics[width=10cm]{show_1.jpg}}
\hspace{0.2in}
\subfigure{
\label{3} %% label for second subfigure
\includegraphics[width=8cm]{areaUnderROC_1.jpg}}
\caption{maxIter=5时预测值和areaUnderROC}
\label{0_1} %% label for entire figure
\end{figure}
\item maxIter=10时areaUnderROC为0.9993。\newline
\begin{figure}[h!]
\centering
\subfigure{
\label{4} %% label for first subfigure
\includegraphics[width=10cm]{show_2.jpg}}
\hspace{0.2in}
\subfigure{
\label{5} %% label for second subfigure
\includegraphics[width=8cm]{areaUnderROC_2.jpg}}
\caption{maxIter=10时预测值和areaUnderROC}
\label{0_1} %% label for entire figure
\end{figure}
\end{itemize}
从实验结果可以看出随着迭代次数的增加算法的效果也是越来越好。以rawPrediction或者probability为例样本取对的类标签的置信度或者概率随着迭代次数的增加不断增加。
\section{库源代码分析}
如前所述Spark 使用scala编写的其Java接口是直接调用的scala实现而Python接口又是基于Java接口实现的因此虽然我报告中使用的Python接口实现的但是算法的具体实现细节是用scala实现的由于本人未曾使用过scala因此在这里根据对代码的直观理解简要的介绍一下其大致思想。
\begin{itemize}
\item 参数设置,算法一开始都是一些基本的对参数的设置,比如迭代次数,规则化参数,是否对数据标准化等等。例如:
\begin{lstlisting}[escapeinside='']
@Since("1.2.0")
def setMaxIter(value: Int):this.type = set(maxIter,value)
setDefault(maxIter -> 100)
...
\end{lstlisting}
\item train()方法进行训练。首先是对训练数据集进行统计便于后续的处理。\newline
\begin{lstlisting}[escapeinside='']
val (summarizer, labelSummarizer) = {
//'此处用于生成相应的统计数据'
}
val histogram = labelSummarizer.histogram
val numInvalid = labelSummarizer.countInvalid
val numClasses = histogram.length
val numFeatures = summarizer.mean.size
\end{lstlisting}
其次是对训练数据集进行一些有效性和特殊性的判断。\newline
\begin{lstlisting}
if (numClasses > 2) {
...
}
\end{lstlisting}
由于在Spark MLlib中ml版本中LogisticRegression还未支持多分类所以它会先判断样本是否是二分类否则会先报异常。如果想用多分类的话可以用mllib版本。我看了下mlib版本的源码它会进行判断如果是二分类它会调用ml版本的实现如果是多分类才会调用自己的实现。此外我发现Spark的帮助文档里有个小缺陷在ml关于LogisticRegression的介绍中他指出算法的详细实现细节可以看mllib的实现但是mllib的实现中关于二分类并没有具体的算法细节反而在ml的实现中是有的。
\begin{lstlisting}
} else if ($(fitIntercept) && numClasses == 2 && histogram(0) == 0.0) {
...
} else if ($(fitIntercept) && numClasses == 1) {
...
}
\end{lstlisting}
上面两个else分支是判断训练数据中的样本是否都是属于正样本否者负样本与此同时如果模型函数中还需要截距的话那么其实不需要进行训练了只要把系数都设为0然后把截距设为正无穷或者负无穷即可。
\begin{lstlisting}
if (!$(fitIntercept) && numClasses == 2 && histogram(0) == 0.0) {
...
} else if (!$(fitIntercept) && numClasses == 1) {
...
}
\end{lstlisting}
上面两段代码也是判断训练数据中的样本是否都是属于正样本否者负样本,但是模型函数中如果不需要截距的话,那么本次训练可能就会出现问题,所以它会进行警告。
\begin{lstlisting}
val optimizer = if ($(elasticNetParam) == 0.0 || $(regParam) == 0.0) {
new BreezeLBFGS[BDV[Double]]($(maxIter), 10, $(tol))
} else {
...
new BreezeOWLQN[Int, BDV[Double]]($(maxIter), 10, regParamL1Fun, $(tol))
}
\end{lstlisting}
这段代码用来选择算法的优化器在mllib的实现中Logistic Regression有两种实现LogisticRegressionWithSGD和LogisticRegressionWithLBFGS并且官方推荐使用LogisticRegressionWithLBFGS。而在最新的ml的版本中已经看不到SGD版本的了他根据用户传参情况使用LBFGS或者OWLQN。
至此,算法的主要逻辑已经分析的差不多了,剩余的就是对模型函数的系数和截距的计算了,比如:
\begin{lstlisting}
val rawCoefficients = state.x.toArray.clone()
var i = 0
while (i < numFeatures) {
rawCoefficients(i) *= { if (featuresStd(i) != 0.0) 1.0 / featuresStd(i) else 0.0 }
i += 1
}
if ($(fitIntercept)) {
(Vectors.dense(rawCoefficients.dropRight(1)).compressed, rawCoefficients.last,
arrayBuilder.result())
} else {
(Vectors.dense(rawCoefficients).compressed, 0.0, arrayBuilder.result())
}
\end{lstlisting}
\end{itemize}
\newpage
\section{总结}
通过本次实验我对Spark尤其是其MLlib库有了初步的了解。在如今大数据盛行的时代Saprk有广泛的应用场景流处理、交互式SQL查询和机器学习等。尤其是在机器学习方面它基于内存的处理架构很适合机器学习的迭代运算。对于Spark来说2015年最重要的变化应该是DataFrame的引入它能够适应更广泛的数据处理要求也吸引了更多的使用者。而随着DataFrame起初的机器学习库mllib也逐渐迁移到基于DataFrame接口的库ml。在本次实验中我感受到的有以下几点
\begin{itemize}
\item Spark官方文档很完善并且讲述的也很详细对我这样的Spark初学者来说有很大的帮助。
\item Spark对API的支持很棒虽然Spark使用scala开发的但是它还提供了几乎一致的针对Java、python和R的接口这能使更多语言背景的开发者轻松的使用Saprk进行开发。
\item 机器学习库工具比较完善虽然目前Saprk MLlib实现的只是一些常用的算法但是它却提供了很多的工具类比如能够方便地对数据进行切分拆分出训练数据和测试数据对于训练的模型也提供了能够进行专业评估的工具类。因此 用Spark开发机器学习应用很方便效率很快。
\end{itemize}
很感谢谭老师给我们布置这样的作业让我接触到了Spark为我以后的研究工作储备了更多的方法。否则我很难有机会主动地去接触这些新鲜的大数据技术通过对Spark的了解熟悉我也对软件工程中的软件设计和架构有了进一步的理解。总之通过本次报告我受益匪浅。
\end{spacing}
\end{document}