6 非结构性数据预处理

非结构化数据是数据结构不规则或者说是不完整,没有预设的数据模型或者结构,不便使用数据库、模型及标准的数据接口表现的数据,包括所有格式的文本、图片、各类报表、图像、音频、视频数据等。

计算机信息化系统中的数据分为结构化数据和非结构化数据。非结构化数据的形式非常多样,标准也具有多样性(即标准不确定),同时在技术上非结构化信息比结构化信息(纯数值信息)更难标准化和理解。

非结构化数据几乎在任何场景中都能得到,包括但不限于如下:

  • 邮件信息、聊天记录及搜集到的结果
  • 网站评论
  • 应用程序中得到的文本字段
  • 短视频App中的视频信息
  • 社交网络中的图文信息
  • 博客、论坛上感兴趣的话题

对以上数据不能通过简单的变换完成数据预处理,前述的预处理方法是一种狭义上的预处理,其处理的数据本质上是数值化的、结构化的,而处理过程主要是一种数值变换的方法为主

而对于非结构化数据的预处理则是广义上的数据预处理,本质是特征提取。在机器学习、模式识别、图像处理中,特征提取是从一些初始的测量数据(原始数值)开始,构建具有信息性和非冗余性的派生值(特征),在某些情况下形成更具有人类解释性的数据。

特征提取与降维有关,当输入到算法中的数据维度太大,无法处理,并且怀疑它是冗余的(比如像素的图像的重复性),因此需要将其转换为一组减少的特征集(即特征向量)。

确定初始特征的子集称为特征选择,我们期望所选择出的特征可以包含来自输入数据的相关信息,这样就可以使用这种简化的表示来代替完整的原始数据执行所需的任务。

对大量变量进行分析会消耗大量的内存和算力,同时可能导致模型算法对训练样本的适应性过强(过拟合),而对新样本的泛化能力较差。特征提取是构造变量组合来解决这些问题,同时对数据的描述仍具有足够的准确性。正确地优化特征提取是有效构建模型的关键。

特征提取是将机器学习算法无法识别的非结构化数据转化为可以识别的特征的过程。比如:由一系列文字组成的文本,文字经过分词后会形成一个词语集合,对于词语集合(非结构化数据),机器学习算法是不能直接使用的,因此需要转化为可识别的数值特征(用固定长度的向量表示);图片是由一系列像素点(原始数据)构成,这些像素点本身是无法被机器学习算法直接使用的,但将这些像素点转化为矩阵(数值形式),机器学习算法就能对其进行训练。

7 文本数据处理

原始的文本数据缺少可解释性,并且数据结构不够规范。通常进行如下两步:

  1. 特征的提取
    将文本中有意义的部分提取出来,通常采用的是分词技术,因为构成文本中最小的信息单元是单词。随着对文本数据的处理不同,单词本身作为维度的载体,其更深层的语义关系还可以进一步挖掘。比如:诗词中单词的平仄韵脚可以作为一个维度;新闻中单词蕴含的感情色彩可以作为一个维度。特征的提取是将有效信息和非冗余的信息从非结构化的原始数据中剥离出来的一个过程。
  2. 对有“意义”的数据(Token)进行结构化变换
    使其满足可以被模型识别的结构化数据,即狭义的预处理过程。数据用向量的形式表现出来的过程可以称为向量化。即对于非结构数据围绕特征提取和狭义数据预处理两个步骤进行。

7.1 分词技术

在英文语境中,对单词的划分可以通过空格及标点符号进行;但在中文中,由于书写习惯的原因,并不存在通过空格的方式将词分隔开,而单字的分隔通常无法表达完整的语义,通过多字单词才能实现完整的语义表达。

所以,基于字的检索按单字建立索引,虽然可以满足基本功能,但运算量过大、检索准确率低;而基于词的检索按词建立索引,检索时直接命中,检索速度快、准确率高,目前中文检索系统多数支持基于词的检索。

Sklearn中自带的库无法实现中文与语境的分词目标,需要借助第三方库实现该需求,比如jieba。

# encoding=utf-8
import jieba
seg_list=jieba.cut(u'模式识别与人工智能',cut_all=True)
print("全模式分词输出如下:")
print(','.join(seg_list))
'''
全模式分词输出如下:
模式,模式识别,识别,与,人工,人工智能,智能
'''
# 精确模式,其默认是精确模式,默认值可以不写
seg_list=jieba.cut('模式识别与人工智能',cut_all=False)
# seg_list=jieba.cut('机器学习从入门到入职') 
print("默认模式分词输出如下:")
print(','.join(seg_list))
'''
默认模式分词输出如下:
模式识别,与,人工智能
'''
print('切割后生成列表如下:')
list=jieba.lcut('模式识别与人工智能')
list
'''
切割后生成列表如下:
['模式识别', '与', '人工智能']
'''

7.2 对已提取的数据的处理

这些被提取出的“有意义”数据,通常是以集合的方式表示的,如字典(Dict)、列表(List)、数组(Array)。此时需要对其进行数字化转化,使其归纳在一个统一的量度当中。

最简单的方法可能是对数据集合进行数字编号,如对一个全是关键字的列表,与其本身的索引(Index)相对应。这是一种快速、简洁的方法,但是存在一个隐患,那就是被提取的数据集合中的元素前提假设是线性独立的。即,当数据以一种紧密具有潜在序列性(隐含的关联在其中)的方式呈现时,破坏了其本身具有的独立性。

假设一个分类问题有如下的分类集合{哺乳类,植物,鸟类,鱼类},如果仅仅用简单的索引表示{0,1,2,3},则在分类的时候,植物与哺乳类之间的关系甚至近于鱼类与哺乳类的关系,这不符合常识。

因此,在数据化过程中还可以保证数据的独立性,稀疏化是一种解决方案,它的前提是向量化。原有的数据集合可看作是一组单维向量的集合,稀疏化的过程就是将单维向量变为多维向量,这些转化后的多维向量形成的矩阵存在一个特性,即绝大多数的元素为0,这样的矩阵称之为稀疏矩阵。

1 独热编码

One-Hot Encoding,又称为一位有效编码,主要思路是用等同于状态数量(有效特征数量)的位数 或者说是维度进行编码,每种状态(特征)下的独热编码其中只有一位是1,其余的为0。

假设有1,2,3三种状态,则对应的独热编码为100,010,001。其意义在于使特征得以扩展(升维),变得稀疏。文本同样可以采用独热编码的方式,假设存在一个大小为N的字典,于是存在位数为N的寄存器,当一段文本中的各个单词被提取之后,将会被N为寄存器字典进行编译,在相应的位置上标记为1,其余的位置上标记为0;当然可能会出现预设的文本字典中不存在的单词,故在设计之初,给缺省的单词一个单独的位置,于是就有了(N+1)位字典寄存器。

未知的随机文本(原始非结构数据)经过预设好的字典集合(映射)得到对应的独热编码(转化后的结构数据)。

from sklearn.feature_extraction import DictVectorizer
# 设置初始词典
city_dict=[{'city':'BeiJing'},{'city':'NanJing'},{'city':'DongJing'},{'city':'XiaMen'}]
# 初始化 字典向量化器,并用对象vec接住
vec=DictVectorizer()
print('将字典拟合再向量化之后,输出结果如下:')
print(vec.fit_transform(city_dict).toarray())
print('将新的字典使用之前生成的向量化器转换,输出结果如下:')
city_dict_new=[{'city':'NanJing'},{'city':'ShangHai'},{'city':'BeiJing'}]
print(vec.transform(city_dict_new).toarray())
# type(vec.transform(city_dict_new))
# scipy.sparse.csr.csr_matrix向量化器转化后返回的是稀疏矩阵
'''
直接转换的话,就是沿用第一个初始化后的向量化器
将字典拟合再向量化之后,输出结果如下:
[[1. 0. 0. 0.]
 [0. 0. 1. 0.]
 [0. 1. 0. 0.]
 [0. 0. 0. 1.]]
将新的字典使用之前生成的向量化器转换,输出结果如下:
[[0. 0. 1. 0.]
 [0. 0. 0. 0.]
 [1. 0. 0. 0.]]
 如果最后一句代码有fit的过程,则会重新生成向量化器进行转换
 将字典拟合再向量化之后,输出结果如下:
[[1. 0. 0. 0.]
 [0. 0. 1. 0.]
 [0. 1. 0. 0.]
 [0. 0. 0. 1.]]
将新的字典使用之前生成的向量化器转换,输出结果如下:
[[0. 1. 0.]
 [0. 0. 1.]
 [1. 0. 0.]]
'''

字典中不同的词在被转换过程中,首先会将其稀疏化变为只有0和1的矩阵(方阵,有n个元素阶数就为n),不同的词越多,其矩阵的列数就越多——每个词都被分配到对应的那一列上,如果在转化的过程中,出现了之前并不存在的词汇,那么所有列都将置为0。

2 哈希技巧(☆ 没看懂)

独热编码有效地保证了数据之间的独立性,但在实际应用过程中存在一定的局限性。例如,文本数据的提取中,所得到的特征词数量可能会非常大,如果使用独热编码,其维度数等于特征词的数目,那么6000个单词就会有6000个维度,这样在运算过程中会占用过多的内存,所以有必要对其进行压缩也就是降维处理。这里的降维方法被称为哈希技巧(Hashing Trick),也可以称为特征哈希(Feature Hashing)。

在哈希技巧中会定义一个经过特征哈希化后对应的一个哈希表,这个哈希表的维度远远小于原始词汇表的特征维度,是一个明显的降维处理。具体的方法是,对于任意一个特征名,可以用哈希函数找到其对应于哈希表中的位置,然后该特征名所对应的词频统计值将会累加到该哈希表的位置上。

存在哈希函数h(x)使第i个特征(特征总数为I)在哈希表上的对应位置是j,则第i个原始特征的词频数值Φ(x(i))将累积到哈希后的第j个特征的词频数值φ(x(j))上,即
$$
\varphi(x_{(j)})=\sum \phi(x_{(i)}),i从1取到I
$$

7.3 文本的特征提取

  • 令牌化(Tokening):给文本以数字ID(token),空格和其他休止符作为token划分的识别器。
  • 词频统计(Counting):对每个出现在文档中的令牌(特征词)做统计。
  • 规则化与权重计算(Normalizing and Weighting):对相关的词赋予权重,每篇文章中都高频出现的词汇,给予降低权重;而突出独立于其他文本的特定文本中出现的高频词汇,给予升高权重。比如,在A,B,C三篇文本中,所有文本中出现高频词如and、is、the等词汇,这种无法展示文本性质的词汇,需要降低权重;而仅在A,B,C中某一篇出现的高频词汇,比如memory、CPU、thread、network等,能够说明该篇文本性质的词汇,则给予升高权重。

1 词频向量化

一个关键词如何与原始文本数据建立关系,是文本特征向量化的关键。文本可以看作是关键词的有序集合,而文本特征提取则会无视这种序列性。可以通过关键词在文本中出现的频率或者分布进行计算,因此针对文本的预处理方法存在两个特点。

  • 每个独立令牌出现的频率被视为特征。
  • 给定文档的所有标记频率的向量被认为是一个多元样本。

因此文档的语料库可以用一个矩阵表示,其中每个文档数据为一行,每个标记(如单词)有一列(特征)记录在语料库中。但这种方式存在一定的局限性,当某些特征词的意义不大时,其在表示文本内容时被认为是没有信息量的,为了避免它们被解释为预测的信号,需要将其删除。然而,有时类似的词是有用的预测,如分类的写作风格或者个性,应视具体情况而定。

针对这种情况,可以设置停止词(Stop Words)集合,即预设一些词汇是没有意义的,当再提取文本特征时,这些无意义的停止词将不被视为关键词而被提取,从而使文本的特征提取更加精准;同时,不止可以采用停止词的方式,也可以通过控制词频等方式进行筛选;在实际文本抽取的需求中,涉及某些特征要以词组的形式存在,可以通过设置N-gram(N元模型)等方式实现词组的抽取。

# 引入词频统计类
from sklearn.feature_extraction.text import CountVectorizer\
# 原始文本数据
text_all=[
    'the hongkong journalist is running faster than anyone else,it is excited,I have been to all western countries,\
    I know pretty much what is good good good at',
    'the hongkong journalist is running faster than anyone else,it is excited good good good',
]
# 初始化无配置的词频统计转化器
vectorizer_origin=CountVectorizer()

# 初始化基于字符的词频统计转化器
vectorizer_analyzer=CountVectorizer(analyzer='char')

# 初始化带有停止词的词频统计转化器,停止词为'is' 'I' 'it'
vectorizer_stop=CountVectorizer(stop_words=['is','I','it'])

# 初始化带有文档频率阈值的词频统计转化器,文档频率最大不超过0.5,最小不低于0.1
vectorizer_counter_filter=CountVectorizer(max_df=0.5,min_df=0.1)

# 初始化带有N-gram范围的词频统计转化器
vectorizer_ngram_range=CountVectorizer(ngram_range=(1,3),
                                      stop_words=['is','I','it','good'])

# ☆ 关于不同策略的转换器集合
strategy={'原始词频统计':vectorizer_origin,
          '基于字符的词频统计':vectorizer_analyzer,
          '基于停止词':vectorizer_stop,
          '基于高-低词频的词频统计':vectorizer_counter_filter,
          '基于N-gram的词频统计':vectorizer_ngram_range,
}
# 根据不同策略的转换器抽取的特征
def show_transformed_data(strategy=strategy):
    for k,v in strategy.items():
        v.fit(text_all)
        print('{0}的特征如下:'.format(k))
        print(v.get_feature_names())

# 观察转换器抽取的特征
show_transformed_data()

2 词频-逆文档频率法

上述方法中设置停止词的方法并不是特别智能,因为需要人工维护一个停止词集合。因此提出了词频-逆文档频率法(Term Frequency-Inverse Document Frequency,TF-IDF)。

TF-IDF是一种加权的词频统计方法。由两部分组成,即词频公式和逆文档频率公式。简单的词频公式并不能准确挖掘出文本的特征,因为有一些高频词实际上没有很大的意义,如"the,is,a"等,与此同时,还有一些有意义的词汇也会高频出现,所以为了区分,采用了加权的方式,具体如下:
$$
tf_idf_{(t,d)}=tf(t,d)×idf(t)
$$
式中,t代表特定的词汇,d代表特定的文档。

tf(t,d)代表指定文档d中包含特定词汇td在该文档中的词频,其中指定文档d的所有词的数量为td_total,其公式为:(某个词的总数量在某篇文档所有词汇量中所占的比重)
$$
tf(t,d)=\frac{t_{d}}{t_{d_total}}
$$
nd代表文档总数量,df(t,d)代表包含特定词汇t的文档数量,(文档总数比上含有某词汇的文档数量)idf(t)的公式为:
$$
idf(t)=lg\frac{n_d}{df(t,d)}
$$
该公式呈现了文档总数与包含特定词汇t的文档数量之间的对数关系,可以广泛地将没有太大意义的词汇的权重降低。主要思路就是一些词汇在越多的文章中出现,那么其意义越小。但是会存在一些隐患,比如某个生僻字在语料库中没有,则其分母df(t,d)为0,IDF没有意义。所以,常用的IDF需要做一些平滑处理,使语料库中没有出现的词也可以得到一个合适的IDF值。

此时引入平滑因子,在分母上添加一个常数项,通过为1,所以idf(t)的公式变为:
$$
idf(t)=lg\frac{1+n_d}{1+df(t,d)}+1
$$
平滑后的文档频率公式不仅可以防止出现分母为0的情况,还对逆文档频率为0的情况进行了平滑处理,因为在逆文档频率为0时,整体就不能表现出其词频特性。

经过TF-IDF处理后的文本被转换成相应的向量集,此时狭义的预处理方法就可以应用,将"长度"参差不齐的向量转换为统一度量的向量——范数规范化。注意:TfidfTransformer不能直接处理文本数据,需要先将其转化为词频统计,或者直接使用TfidfVectorizer实现,代码为:(☆ 结果的意义没看懂)

from sklearn.feature_extraction.text import TfidfTransformer,TfidfVectorizer,CountVectorizer
import numpy as np
transformer=TfidfTransformer(smooth_idf=False)
vectorizer_nostop=CountVectorizer(stop_words=[])
# vectorizer_nostop=CountVectorizer() 和上句代码效果一样
tfidfVector=TfidfVectorizer(smooth_idf=False)
text=['this is an apple','this is a pen','this is','apple pen']

print('使用tfidf转换器实现文本数据提取:') #必须先转化词频统计
print(transformer.fit_transform(vectorizer_nostop.fit_transform(text)))

print('使用tfidf向量化器实现文本数据提取:') # 建议直接使用这个 
print(tfidfVector.fit_transform(text))
'''
使用tfidf转换器实现文本数据提取:
  (0, 4)	0.37363537486070153
  (0, 2)	0.37363537486070153
  (0, 1)	0.4912856170299323
  (0, 0)	0.6924100344484653
  (1, 4)	0.5178561161676974
  (1, 3)	0.680918560398684
  (1, 2)	0.5178561161676974
  (2, 4)	0.7071067811865476
  (2, 2)	0.7071067811865476
  (3, 3)	0.7071067811865476
  (3, 1)	0.7071067811865476
使用tfidf向量化器实现文本数据提取:
  (0, 1)	0.49128561702993234
  (0, 0)	0.6924100344484653
  (0, 2)	0.3736353748607016
  (0, 4)	0.3736353748607016
  (1, 3)	0.680918560398684
  (1, 2)	0.5178561161676974
  (1, 4)	0.5178561161676974
  (2, 2)	0.7071067811865476
  (2, 4)	0.7071067811865476
  (3, 3)	0.7071067811865476
  (3, 1)	0.7071067811865476
'''
  • (文档编号,文档中对应的词编号)→该词转换后的值
  • (0,2),如0.5表示编号为0的文档中编号为2的特征词的TF-IDF值是0.5。

3 自定义向量器

Sklearn提供了自定义向量器,其结构主要包含3个部分:

  • 数据预处理器(Preprocessor)——将整个文档作为输入(作为单个字符串)并返回可能转换的文档版本(仍然是整个字符串)的可调用函数,可以用于删除HTML标记、小写整个文档等。
  • 令牌生成器(Tokenizer)——从预处理器获取输出并将其拆分成令牌,之后返回这些标记的列表的一个可调用函数。
  • 分析器(Analyzer)——替换预处理程序和令牌程序的可调用函数,因为默认的分析器都会调用预处理器和令牌器,但是自定义分析器将跳过此操作

大多数文本处理方法是基于英文场景设计的,中文分词则会被分割成独立的字,所以一般是结合第三方中文文本处理库jieba共同使用。

8 图形的特征提取

由于图片的大小或者维度(2D或者3D)不同,在处理图形之前,要对图形的特征结构进行规范化。

  • 补丁提取——将整个图片切割成相应的图块,从而形成大小一致的矩阵。
  • 连接矩阵转化——有些模型需要将图形上像素间的连接关系呈现出来,因此需要使用连接矩阵转化,将图片信息上的像素转化为连接矩阵。
import numpy as np
from sklearn.feature_extraction import image
# 生成一个大小为3*3的彩色图片矩阵
one_image=np.arange(3*3*3).reshape((3,3,3))
print('大小为3*3的彩色图片的黄色通道输出如下:')
# print(one_image[:,:,0])
print(one_image[:,:,1])
# print(one_image[:,:,2])
# print(one_image[:,:,:])
# 将图片裁剪成2*2大小的补丁图片,最多生成4个补丁
patches=image.extract_patches_2d(one_image,(2,2),max_patches=4,random_state=0)
print('大小为2*2的彩色补丁图片的黄色通道输出如下:')
print(patches[:,:,:])
print('=============')
print(patches[:,:,1])

鉴于图形的特征提取是一个十分复杂的过程,而Sklearn能够提取的特征其意义十分有限,无法满足实际的图形处理应用场景,如人脸识别、物体识别等,所以不会将其应用于图形特征提取。深度网络模型才更适合图形处理