app开发者平台在数字化时代的重要性与发展趋势解析
1026
2022-08-23
读书笔记:Spark上数据的获取,处理与准备 下
感想
本章有点长,所以分成了两部分,前面讲了推荐数据获取和一些统计可视化的结果,后面就讲这些数据的处理,然后变成机器学习模型的输入的过程和示例。
3. 处理与转换数据
为了让原始数据可用于机器学习算法,需要先对其进行清理,并可能需要将其进行各种转换,之后才能从转换后的数据里提取有用的特征。数据的转换和特征提取联系紧密。某些情况下,一些转换本身便是特征提取的过程。
在之前处理电影数据集时我们已经看到数据清理的必要性。一般来说,现实中的数据会存在信息不规整,数据点缺失和异常值问题。理想情况下,我们会修复非规整数据。但很多数据集都源于一些难以重现的收集过程(比如网络活动数据和传感器数据),故实际上会难以修复。值缺失和异常也很常见,且处理方式与处理非规整信息类似。总的来说,大致的处理方法如下。
· 过滤掉或删除非规整或有缺失的数据:这通常是必须的,但的确会损失这些数据里那些好的信息。
· 填充非规整或缺失的数据:可以根据其他的数据来填充非规整或缺失的数据。方法包括用零值,全局期望或中值来填充,或是根据相邻或类似的数据点来做插值(通常针对时序数据)等。选择正确的方式并不容易,它会因数据,应用场景和个人经验而不同。
· 对异常值做鲁棒处理:异常值的主要问题在于即使它们的极值也不一定就是错的。到底是对是错通常很难分辨。异常值可被移除或是填充,但的确存在某些统计技术(如鲁棒回归)可用于处理异常值或是极值。
· 对可能的异常值进行转换:另一种处理异常值或极值的方法是进行转换。对那些可能存在异常值或值域覆盖过大的特征,利用如对数或高斯核对其转换。这类转换有助于降低变量存在的值跳跃的影响,病将非线性关系变为线性的。
非规整数据和缺失数据的填充
前面已经举过过滤非规整数据的例子,顺着上述代码,下面的代码对发行日期有问题的数据采取了填充策略,即用发行日期的中位数来填充问题数据。
years_pre_processed = movie_fields.map(lambda fields: fields[2]).map(lambda x: convert_year(x)).filter(lambda yr: yr != 1900).collect()years_pre_processed_arr = np.array(years_pre_processed)
在选取所有的发行日期后,这里首先计算发行年份的平均数和中位数。选取的数据不包含非规整数据。然后用numpy的函数找出year_pre_processed_array中的非规整数据点的序号(之前我们给该数据点分配了1900的值)。最后通过该序号来将中位数作为非规整数据的发行年份:
# first we compute the mean and median year of release, without the 'bad' data pointmean_year = np.mean(years_pre_processed_arr[years_pre_processed_arr!=1900])median_year = np.median(years_pre_processed_arr[years_pre_processed_arr!=1900])idx_bad_data = np.where(years_pre_processed_arr==1900)[0][0]years_pre_processed_arr[idx_bad_data] = median_yearprint "Mean year of release: %d" % mean_yearprint "Median year of release: %d" % median_yearprint "Index of '1900' after assigning median: %s" % np.where(years_pre_processed_arr == 1900)[0]
(这里的几行代码,我没有跑通)这里同时求出了发行年份的平均值和中位值。
从输出也可以看到,发行年份分布的偏向使得其中位数值很高。特定情况下通常不容易确定选取什么样的值来做填充才够精确。但在本例中,从该偏向来看使用中位值来填充的确可行。
4. 从数据中提取有用特征
在完成对数据的初步探索,处理和清理后,便可从中提取可供机器学习模型训练用的特征。
特征(feature)指那些用于模型训练的变量。每一行数据包含可供提取到样本训练中的各种信息。从根本上说,几乎所有机器学习模型都是与用向量表示的数值特征打交道;因此,我们需要将原始数据转换为数值。
特征可以概括分为如下几种:
· 数值特征:这些特征通常为实数或整数,比如之前例子中提到的年龄。
· 类别特征:它们取值只能是可能状态集合中的某一种。我们数据集中的用户性别,职业或电影类别便是这类。
· 文本特征:它们派生自数据中的文本内容,比如电影名,描述或是评论。
· 其他特征:大部分其他特征都最终表示为数值。比如图像,视频和音频可被表示为数值数据的集合。地理位置则可由经纬度或地理散列表示。
4.1数值表示
原始的数值和一个数值特征之间的区别是什么?实际上,任何数值数据都能作为输入变量。但是,机器学习模型中所学习的是各个特征所对应的向量的权值。这些权值在特征值到输出或是目标变量(指在监督学习模型中)的映射过程中扮演重要角色。
由此我们会想使用那些合理的特征,让模型能从这些特征学到特征值和目标变量之间的关系。比如年龄就是一个合理的特征。年龄的增加和某项支出之间可能就存在直接关系。类似地,高度也是一个可直接使用的数值特征。
当数值特征仍处于原始形式时,其可用性相对较低,但可以转化为更有用的表示形式。位置信息便是如此。若使用原始位置信息(比如用经纬度表示的),我们的模型可能学习不到该信息和某个输出之间的有用关系,这就使得该信息的可用性不高,除非数据点的确很密集。然而若对位置进行聚合或挑选后(比如聚焦为一个城市或国家),便容易和特定输出之间存在某种关联了。
4.2 类别特征
当类别特征仍为原始形式时,其取值来自所有可能取值所构成的集合而不是一个数字,故不能作为输入。如之前的例子中的用户职业便是一个类别特征变量,其可能取值有学生,程序员等。
这样的类别特征也称作名义(nominal)变量,即其各个可能取值之间没有顺序关系。相反,那些存在顺序关系的(比如评级,评级5会高于或者好于评级1)则被称为有序(ordinal)变量。
将类别特征表示为数字形式,常可借助k至1(1-of-k)方法进行。将名义变量表示为可用于机器学习任务的形式,会需要借助如k之1编码这样的方法。有序变量的原始值可能直接使用,但也常会经过和名义变量一样的编码处理。
假设变量可取的值有k个。如果对这些值用1到k编序,则可以用长度为k 的二元向量来表示一个变量的取值。在这个向量里,该取值对应的序号所在的元素为1,其他元素都为0.
比如,我们可以取回occupation的所有可能取值:
all_occupations = user_fields.map(lambda fields: fields[3]).distinct().collect()all_occupations.sort()
然后可以依次对各可能的职业分配序号:
# create a new dictionary to hold the occupations, and assign the "1-of-k" indexesidx = 0all_occupations_dict = {}for o in all_occupations: all_occupations_dict[o] = idx idx +=1# try a few examples to see what "1-of-k" encoding is assignedprint "Encoding of 'doctor': %d" % all_occupations_dict['doctor']print "Encoding of 'programmer': %d" % all_occupations_dict['programmer']
输出为:
Encoding of 'doctor': 2Encoding of 'programmer': 14
最后来编码programmer的取值。首先需创建一个长度和可能的职业数目相同的numpy数组,其各元素值为0。这可通过numpy的zeros函数的实现。
之后将提取单词programmer的序号,并将数组中对应该序号的那个元素值赋为1:
# create a vector representation for "programmer" and encode it into a binary vectorK = len(all_occupations_dict)binary_x = np.zeros(K)k_programmer = all_occupations_dict['programmer']binary_x[k_programmer] = 1print "Binary feature vector: %s" % binary_xprint "Length of binary vector: %d" % K
输出为:
Binary feature vector: [ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.] Length of binary vector: 21
4.3 派生特征
从现有的一个或多个变量派生出新的特征常常是有帮助的。理想情况下,派生出的特征能比原始属性带来更多的信息。
比如,可以分别计算各用户已有的电影评级的平均数。这将能给模型加入针对不同用户的个性化特征(常用于推荐系统)。在前文中我们也从原始的评级数据里创建了新的特征以学习出更好的模型。
从原始数据派生特征的例子包括计算平均值,中位值,方差,和,差,最大值或最小值以及计数。在先前的内容中,我们也看到是如何从电影的发行年份和当前年份派生了新的movie age特征的。这类转换背后的想法常常是对数值数据进行某种概括,并期望它能让模型学习更容易。
数值特征到类别特征的转换也很常见,比如划分为区间特征。进行这类转换的变量常见的有年龄,地理位置和时间。
将时间戳转为类别特征
下面以对评级时间的转换为例,说明如何将数值数据装换为类别特征。该时间的格式为Unix的时间戳。我们可以用Python的datetime模块丛中提取出日期,时间以及点钟信息。其结果将是由各评级对应的点钟数所构成的RDD。
需要定义一个函数将评级时间戳取为datetime的格式:
# a function to extract the timestamps (in seconds) from the datasetdef extract_datetime(ts): import datetime return datetime.datetime.fromtimestamp(ts)
我们首先使用map将时间戳属性转换为Python int类型。然后通过extract_datetime函数将各时间戳转为datetime类型的对象,进而提取出其点钟数。
timestamps = rating_data.map(lambda fields: int(fields[3]))hour_of_day = timestamps.map(lambda ts: extract_datetime(ts).hour)hour_of_day.take(5)
输出为:
[23, 3, 15, 13, 13]
这就完成了从原始数据到表示评级发生的点钟的类别特征的转换。
现在,假设我们觉得这样的表示过于粗糙,想更为精确。我们可以将点钟数划分到一天中的不同时段。比如可以说7点到12点是上午,12点到14点是中午,以此类推。要生成这些时间段,可以创建一个以点钟数为输入的函数来返回相应的时间段:
# a function for assigning "time-of-day" bucket given an hour of the daydef assign_tod(hr): times_of_day = { 'morning' : range(7, 12), 'lunch' : range(12, 14), 'afternoon' : range(14, 18), 'evening' : range(18, 24), 'night' : range(0, 7) } for k, v in times_of_day.iteritems(): if hr in v: return k# now apply the "time of day" function to the "hour of day" RDDtime_of_day = hour_of_day.map(lambda hr: assign_tod(hr))time_of_day.take(5)
输出为:
['evening', 'night', 'afternoon', 'lunch', 'lunch']
注意,实验的时候可能会输出None, None对应的是上面的边界值,如24,7,12等,所以输出了None。所以要注意区间的划分。
我们已将时间戳变量转为点钟数,再接着转为了时间段,从而得到了一个类别特征。我们可以借助之前提到的k之1编码方法来生成相应的二元特征向量。
4.4 文本特征
从某种意义上讲,文本特征也是一种类别特征或派生特征。下面以电影的描述来举例。即便作为类别数据,其原始的文本也不能直接使用。因为假设每个单词都是一种可能的取值,那单词之间可能出现的组合有几乎无限种。这时模型几乎看不到有相同的特征出现两次,学习的效果也就不理想。从中可以看出,我们会希望将原始的文本转换为一种便于机器学习的形式。
文本处理的方式有很多种。NLP便是专注于文本内容的处理,表示和建模的一个领域。这里介绍一种简单且标准化的文本特征提取方法。该方法被称为词袋(bag-of-word)表示法。
词袋法将一段文本视为由其中的文本或数字组成的集合,其处理过程如下。
· 分词(tokenization):首先会应用某些分词方法将文本分隔为一个由词(单词,数字等)组成的集合。可用的方法如空白分割法。这种方法在空白处对文本分隔病可能还删除其他如标点符号和其他非字母或数字字符。
· 删除停用词(stop words removal):之后,它通常会删除常见的单词,比如the,and和but等。
· 提取词干(stemming):下一步则是词干的提取。这是指将各个词简化为其基本的形式或者干词。常见的例子如复数变为单数(如dogs变为dog等)。提取的方法有很多种,文本处理算法库中常常会包括多种词干提取方法。
· 向量化(vectorization):最后一步就是用向量来表示处理的词。二元向量可能是最为简单的表示方式。它用1和0来分别表示是否存在某个词。从根本上说,这与之前提到的k之1编码相同,与k之1相同,它需要一个词的词典来实现词到索引序号来映射。随着遇到的词增多,各种词可能达数百万。由此,使用稀疏矩阵来表示很关键。这种表示只记录某个词是否出现过,从而节省内存和磁盘空间,以及计算时间。
提取简单的文本特征
我们以数据集中的电影标题为例,来示范如何提取文本特征为二元矩阵。
首先创建一个函数来过滤掉电影标题中可能存在的发行年月。如果标题中存在发行年月,就只保留电影的名称。
我们使用正则表达式模块re来寻找标题里位于括号之间的年份。如果找到与表达式匹配的字段,我们将提取标题中匹配起始位置(即左括号所在的位置)之前的部分。
代码:
# we define a function to extract just the title from the raw movie title, removing the year of releasedef extract_title(raw): import re grps = re.search("\((\w+)\)", raw) # this regular expression finds the non-word (numbers) between parentheses if grps: return raw[:grps.start()].strip() # we strip the trailing whitespace from the title else: return raw
之后从movie_fields RDD里提取出原始的电影标题:
# first lets extract the raw movie titles from the movie fieldsraw_titles = movie_fields.map(lambda fields: fields[1])# next, we strip away the "year of release" to leave us with just the title text# let's test our title extraction function on the first 5 titlesfor raw_title in raw_titles.take(5): print extract_title(raw_title)
输出为:
Toy StoryGoldenEyeFour RoomsGet ShortyCopycat
下面对原始标题调用该函数,并调用一个分词法来将处理后的标题转为词。这里会使用之前提到的简单空白分词法:
# ok that looks good! let's apply it to all the titlesmovie_titles = raw_titles.map(lambda m: extract_title(m))# next we tokenize the titles into terms. We'll use simple whitespace tokenizationtitle_terms = movie_titles.map(lambda t: t.split(" "))print title_terms.take(5)
输出为:
[[u'Toy', u'Story'], [u'GoldenEye'], [u'Four', u'Rooms'], [u'Get', u'Shorty'], [u'Copycat']]
各标题都以空白进行了分隔,从而使得各个单词成为了一个词。
(这里没有谈到一些处理细节,比如将文本转为小写,删除如标点符号和特殊字符之类的非单词或非数字字符,删除连接词和词干提取。这些步骤在现实生活中也非常重要,后章节面会讲到)
我们需要创建一个词字典,来实现到一个整数序号的映射,以便能为每一个词分配一个对应到向量元素的序号。
这里首先使用Spark的flatMap函数来拓展title_termsRDD中每个记录的字符串列表,以得到一个新的字符串RDD。该RDD的每个记录是一个名为all_terms的词。
之后取回所有不同的词,并给他们分配序号。
# next we would like to collect all the possible terms, in order to build out dictionary of term <-> index mappingsall_terms = title_terms.flatMap(lambda x: x).distinct().collect()# create a new dictionary to hold the terms, and assign the "1-of-k" indexesidx = 0all_terms_dict = {}for term in all_terms: all_terms_dict[term] = idx idx +=1num_terms = len(all_terms_dict)print "Total number of terms: %d" % num_termsprint "Index of term 'Dead': %d" % all_terms_dict['Dead']print "Index of term 'Rooms': %d" % all_terms_dict['Rooms']
输出为:
Total number of terms: 2645Index of term 'Dead': 147Index of term 'Rooms': 1963
最后一步是创建一个函数。该函数将一个词集合转换为一个稀疏向量的表示。开始,我们会创建一个空白稀疏矩阵。该矩阵只有一行,列数为字典的总词数。之后我们会逐一检查输入集合中的每一个词,看它是否在词字典中。如果在,那就给矩阵相应序数位置的向量赋值1:
# this function takes a list of terms and encodes it as a scipy sparse vector using an approach # similar to the 1-of-k encodingdef create_vector(terms, term_dict): from scipy import sparse as sp x = sp.csc_matrix((1, num_terms)) for t in terms: if t in term_dict: idx = term_dict[t] x[0, idx] = 1 return x
之后,对提取出的各个词的RDD的各记录都应用该函数。
all_terms_bcast = sc.broadcast(all_terms_dict)term_vectors = title_terms.map(lambda terms: create_vector(terms, all_terms_bcast.value))term_vectors.take(5)
现在可得到新稀疏向量RDD前几条记录如下:
[<1x2645 sparse matrix of type '
现在每一个电影标题都被转换为一个稀疏向量。可以看到那些提取出了2个词的标题所对应的向量里也是2个非零元素,而只提取了1个词的则只对应到了1个非零元素,等等。
4.5 正则化特征
在将特征提取为向量形式后,一种常见的与处理方式是将数值数据正则化(normalization)。其背后的思想是将各个数值特征进行转换,以将它们的值域规范到一个标准的区间内。正则化的方法如下:
· 正则化特征:这实际上是对数据集中的单个特征进行转换。比如减去平均值(特征对齐)或是进行标准的正则转换(以使得特征的平均值和标准差分别为0和1).
· 正则化特征向量:这通常是对数据中的某一行的所有特征进行转换,以让转换后的特征向量的长度标准化。也就是缩放向量中的各个特征以使得向量的范数为1(常指一阶或二阶范数)。
下面将用第二种情况举例说明,向量正则化可通过numpy的norm函数来实现。具体来说,先计算一个随机向量的二阶范数,然后让向量中的每个元素都除该范数,从而得到正规化后的向量:
np.random.seed(42)x = np.random.randn(10)norm_x_2 = np.linalg.norm(x)normalized_x = x / norm_x_2print "x:\n%s" % xprint "2-Norm of x: %2.4f" % norm_x_2print "Normalized x:\n%s" % normalized_xprint "2-Norm of normalized_x: %2.4f" % np.linalg.norm(normalized_x)
输出为:
x:[ 0.49671415 -0.1382643 0.64768854 1.52302986 -0.23415337 -0.23413696 1.57921282 0.76743473 -0.46947439 0.54256004]2-Norm of x: 2.5908Normalized x:[ 0.19172213 -0.05336737 0.24999534 0.58786029 -0.09037871 -0.09037237 0.60954584 0.29621508 -0.1812081 0.20941776]2-Norm of normalized_x: 1.0000
用MLib正则化
Spark在其MLib机器学习库中内置了一些函数用于特征的缩放和标准化,它们包括供标准正态变换的StandardScaler,以及提供与上述相同的特征向量正则化的Normalizer。
首先导入所需要的类,初始化Normalizer(其默认使用与之前相同的二阶范数)。用Spark时,大部分情况下Normalizer所需的输入为一个RDD(它包含numpy数值或MLib向量)。作为举例,我们会从x向量创建一个单元素的RDD。之后将会对我们的RDD调用Normalizer的transform函数。由于该RDD只含有一个向量,可通过first函数来返回向量到驱动程序。接着调用toArray函数来将向量转换为numpy数组。
from pyspark.mllib.feature import Normalizernormalizer = Normalizer()vector = sc.parallelize([x])normalized_x_mllib = normalizer.transform(vector).first().toArray()print "x:\n%s" % xprint "2-Norm of x: %2.4f" % norm_x_2print "Normalized x MLlib:\n%s" % normalized_x_mllibprint "2-Norm of normalized_x_mllib: %2.4f" % np.linalg.norm(normalized_x_mllib)
输出为:
x:[ 0.49671415 -0.1382643 0.64768854 1.52302986 -0.23415337 -0.23413696 1.57921282 0.76743473 -0.46947439 0.54256004]2-Norm of x: 2.5908Normalized x MLlib:[ 0.19172213 -0.05336737 0.24999534 0.58786029 -0.09037871 -0.09037237 0.60954584 0.29621508 -0.1812081 0.20941776]2-Norm of normalized_x_mllib: 1.0000
其结果会和之前用我们自己代码时的完全相同。但使用MLi b内置的函数无疑会更方便和高效。
4.6 用软件包提取特征
虽然上面已经提到了不少特征提取的方法,但每次都要为这些常见任务编写代码并不轻松。当然,我们可以为之创建可充用代码库。更好的方法是可以依赖现有的工具和软件包。
Spark支持Scala,Java, Python的绑定。我们可以通过这些语言所开发的软件包,借助其中完善的工具箱来实现特征的处理和提取,以及向量表示。特征提取可借助的软件包邮scikit-learn, gensim、scikit-image、matplotlib、Python的NLTK、Java编写的OpenNLP以及用Scala编写的Breeze和Chalk。实际上,Breeze自Spark 1.0开始就成为Spark的一部分了。
5. 参考文献
[1]. 直方图中bins应如何理解及处理.
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~