数据挖掘#特征工程(一)总结
1. Overview:
2. 特征选择
主要凭借对业务本身的理解和建模来定的。
1 向前贪心选择
特征子集X初始化为空集。
当该特定变量在经过交叉验证后的Area Under Curve(AUC)值高于现有值时,则将其纳入集合中。
该过程持续直至所有未被考虑过的变量都无法在交叉验证后的AUC上带来提升为止。
其主要缺陷在于仅增加而不再进行调整。
def cv_loop(X, y, model, N):
mean_auc = 0.
for i in range(N):
X_train, X_cv, y_train, y_cv = cross_validation.train_test_split(
X, y, test_size=.20,
random_state = i*SEED)
model.fit(X_train, y_train)
preds = model.predict_proba(X_cv)[:,1]
auc = metrics.auc_score(y_cv, preds)
print "AUC (fold %d/%d): %f" % (i + 1, N, auc)
mean_auc += auc
return mean_auc/N
score_hist = []
N = 10
good_features = set([])
# Greedy feature selection loop
while len(score_hist) < 2 or score_hist[-1][0] > score_hist[-2][0]:
scores = []
for f in range(len(Xts)):
if f not in good_features:
feats = list(good_features) + [f]
Xt = sparse.hstack([Xts[j] for j in feats]).tocsr()
score = cv_loop(Xt, y, model, N)
scores.append((score, f))
print "Feature: %i Mean AUC: %f" % (f, score)
good_features.add(sorted(scores)[-1][1])
score_hist.append(sorted(scores)[-1])
print "Current features: %s" % sorted(list(good_features))
# Remove last added feature from good_features
good_features.remove(score_hist[-1][1])
2 遗传算法
随后系统会自动抽取一批新的特征子集,并计算出每个样本对应的评估指标值;接着采用交叉重组和基因突变等变异算子生成新的种群;其中具有较高评估指标值的个体被选中的概率显著高于低分者;经过连续T次迭代进化后;最终能够获得最高评估指标值的个体将被确定为最优解。评估指标值可以通过交叉验证得分来进行量化评估
# Encode the data and keep it in a list for easy access during feature selection
OHs = [OneHotEncoder(X[:,[i]]) for i in range(X.shape[1])]
Xts = [o[0] for o in OHs]
getX = lambda gne: sparse.hstack([Xts[i] for i in find(gne)]).tocsr()
def mutate(gene, mutation_rate=1e-2):
"""
Mutation method. Randomly flips the bit of a gene segment at a rate of
mutation rate
"""
sel = random.rand(len(gene)) <= mutation_rate
gene[sel] = -gene[sel]
return gene
class Gene(Individual):
"""
Gene class used for feature selection
Implements a fitness function and reproduce function as required by the
Individual base class
"""
def fitness(self):
"""
The fitness of an group of features is equal to 1 - mean(cross val scores)
"""
cv_args = {'X': getX(self.gene),
'y': y,
'score_func': metrics.auc_score,
'cv': cv,
'n_jobs': N_JOBS}
cv_scores = cross_validation.cross_val_score(model, **cv_args)
return 1 - mean(cv_scores)
def reproduce(self, other, n_times=1, mutation_rate=mutation_rate):
"""
Takes another Gene and randomly swaps the genetic material between
this gene and other gene at some cut point n_times. Afterwords, it
mutates the resulting genetic information and creates two new Gene
objects as children
The high level description:
copy the genes from self and other as g1, g2
do n_times:
randomly generate integer j along the length of the gene
set g1 to g1[:j] + g2[j:]
set g2 to g2[:j] + g1[j:]
mutate g1 and g2
return [Gene(g1), Gene(g2)]
"""
lg = len(self.gene)
g1 = copy(self.gene)
g2 = copy(other.gene)
for i in xrange(n_times):
j = random.randint(lg)
g1 = hstack((g1[:j], g2[j:]))
g2 = hstack((g2[:j], g1[j:]))
g1 = mutate(g1, mutation_rate)
g2 = mutate(g2, mutation_rate)
return [Gene(g1), Gene(g2)]
# Do the feature selection
print "Creating the initial gene pool... be patient"
n_genes = ga_args['population_size']
start_genes = (random.rand(n_genes, len(Xts)) > 0.5).astype(bool)
start_genes = sorted([Gene(g) for g in start_genes])
print 'Running the genetic algorithm...'
gene_pool = ga.evolve(start_genes, n_generations)
3 dropout
深度学习中常用的正则化方法,随机丢掉一些特征,防止过拟合。
ind = [ i for i in self._indices(x)] # x: feature, a list of indices
if dropout == 1:
dropped = None
else:
dropped = [random.random() > dropout for i in xrange(0,len(ind))]
#######predict stage########
for j, i in enumerate(self._indices(x)):
if dropped != None and dropped[j]:
continue
wTx += w[i]
if dropped != None:
wTx /= dropout #keep rate
#######update parameters######
if dropped != None and dropped[j]:
continue #不更新参数
3. 特征分析
构建完成的特征基于 brainstorming 的结果难以精确评估现实环境中哪些特征更为关键
重要特征通常包含大量信息,并且必须想办法让模型...更好地学习
找到这些更加重要的信息:
对于规模不太大的数据集而言,Python的scikit-learn包能够有效地计算feature_importance_。
经过创造性思维想出来的特征可能其中一些作用不那么显著, 冗余的特征可能会削弱模型预测能力
通常情况下,这一过程对结果的改善空间相对较小。更重要的是通过深入分析各特征及其相互关系来帮助你全面理解问题的本质。
在sklearn.feature_selection模块中提供了丰富多样的方法作为参考资料
RFECV方法比较暴力,可以试试,但是计算量较大,数据量大的时候慎用。
该方法的核心理念是基于特征选择机制来优化模型性能。具体而言,我们首先关注一个由d个特征组成的集合,并探讨其所有可能的组合情况.根据数学原理,这些子集中共有2^d - 1个非空组合(包括空组合)。为了实现有效的特征筛选,我们需要采用一种外部学习算法,在这一框架下系统地计算每个候选组合对应的验证误差指标.最终,我们通过比较各候选组合的表现,选取验证误差最小的那个组合作为最优特征集合。
4. 特征离散化
主要是暴露重要特征中的信息:
就具有较高权重的特征而言,在其重要性较高的条件下进行离散化处理,并引入更多特征以模拟该模型可能是非线性的。
DNN,GBDT等模型在提取非线性特征方面有一定优势
However, in the realm of deep learning theories, when a function can be compactly represented by a deep architecture, it may necessitate an excessively large architecture to represent it using an insufficiently deep structure. Therefore, maintaining attention to these depth learning models remains highly significant.
1.Hashing Trick

具体做法:
通过Hash函数对特征值(v)进行哈希计算得到h(v),随后执行取模运算得到i = h(v) mod d的结果值。这里d代表目标维度的数量,在此运算后i对应某个特定的维度编号,在该维度上记录的频率即为该哈希操作落在该维的数量。
对于单个属性(单特征),其表示形式为v = str(f: v),其中f代表属性名称(feature name),而v则代表该属性的具体值(feature value)。
对于由多个属性组成的组合特征,则表示形式为v = str(f1_f2: v1_v2),其中f1_f2表示组合属性名(feature name),而v1_v2则代表对应的数值(value)。
通过Hash函数将特征映射至一个固定长度的哈希表之后,并对其进行one-hot编码处理,则可使得特征维度得以固定下来。在此过程中可能会出现碰撞现象,在某些情况下这种碰撞反而是降维的一种表现形式,并且能够进一步提升性能
hash trick的特点:
- 能够维持原有稀疏性的特点
- 在不增加哈希空间维度的情况下允许引入新原始属性
- 方案存在局限性在于经过哈希处理后的模型难以验证其内部机制...
- 将连续变量直接离散化视为分类处理是一种简化手段
- 可以选择对部分原始属性进行哈希映射操作而保留其余属性(例如那些在发生碰撞时会对精度产生显著影响的关键属性)
# for simplicity, we treat both integer and categorical features as categorical
# INPUT:
# csv_row: a csv dictionary, ex: {'Lable': '1', 'I1': '357', 'I2': '', ...}
# D: the max index that we can hash to
# OUTPUT:
# x: a list of indices that its value is 1
def get_x(csv_row, D):
fullind = []
for key, value in csv_row.items():
s = key + '=' + value
fullind.append(hash(s) % D)
if interaction == True:
indlist2 = []
for i in range(len(fullind)):
for j in range(i+1,len(fullind)):
indlist2.append(fullind[i] ^ fullind[j]) # Creating interactions using XOR
fullind = fullind + indlist2
x = {}
x[0] = 1 # 0 is the index of the bias term
for index in fullind:
if(not x.has_key(index)):
x[index] = 0
x[index] += 1
return x # x contains indices of features that have a value as number of occurences
便于记录原始特征与处理后特征之间的对应关系,在数据处理过程中我们可以将其以列表形式存储起来。举个例子来说,在机器学习模型训练中经常需要对数据进行哈希处理以减少计算复杂度,在这个过程中我们就可以将原始特征的ID与处理后的特征ID之间的映射关系记录下来
with_hash = lambda x, D: (x, hash(x, D))
l = [with_hash("%s=%s" % (i,x[i]), D) for i in x]
if self.interactions:
k = x.keys()
v = x.values()
L = len(k)
for i in xrange(0, L):
l.extend([with_hash("%s=%s+%s=%s" % (k[i], v[i], k[j], v[j]), D) for j in xrange(i+1, L)])
特征哈希后续优化:
- 类别特征:频率较低的类别被统一编码为其他类别。
- 连续特征:截断操作限定在min和max这两个区间之间,并对数值较大的特征执行如下转换:v := floor(log(v)²)。
- 可以先用fealib进行离散化处理(建议将区间划分得更加精细)。
- 建议在fealib的基础上先进行离散组合(可能需要更多的参数设置),然后再进行哈希运算。
- 对于连续型特征先采用fealib进行离散化处理,在此基础之上再实施哈希运算。
- 在正则化过程中需要注意除以向量的L2范数以达到归一化效果。
5. 特征组合,加新特征
采用配对组合的方法处理离散特征,并可采用联合分段策略(如joint binning)处理连续特征。例如,在实际应用中常用k-d树来进行这种分段。
1 哈希法:将多个特征的值放到一个元组里,再用一个哈希函数计算其值
def group_data(data, degree=3, hash=hash):
"""
numpy.array -> numpy.array
Groups all columns of data into all combinations of triples
"""
new_data = []
m,n = data.shape
for indicies in combinations(range(n), degree):
new_data.append([hash(tuple(v)) for v in data[:,indicies]])
return array(new_data).T
根据数据中各组合所具有的特征id属性来进行分组,并以这些组的id值作为新的特征属性
def group_data(data, degree=3):
new_data = []
m,n = data.shape
for indices in combinations(range(n), degree):
group_ids = all_data.groupby( \
list(all_data.columns[list(indices)])) \
.grouper.group_info[0]
new_data.append(group_ids)
return array(new_data).T
3 引入率的概念,如差评数/订单数
经过这一阶段的学习与实践, 模型的进化确实面临着瓶颈, 同时在对整个问题的物理模型有了更深的理解后, 通过分析特征重要性等方法进一步加深了对业务运作机制以及数据内在规律的认知
这一策略将现有特征进行系统化归类被视为一个有效的方法。通过采用不同的分类维度能够帮助您优化思考过程,并识别出某些关键信息可能在先前的特征中尚未被充分提取。或者 feature本身的表示是不准确的。
逐一分析每个 feature 的含义及其计算依据,并评估其合理性以及对结果的影响。同时探讨与其他 feature 之间的关联性及其潜在影响。
多样化的feature能够通过结合形成新的特征,并且这一过程同时需要依赖于特征分析以及大量的实证研究
若在现有的特征组合中未能充分表现某些信息,则需引入新的相关特征
6. 换模型
在讨论特征工程时这是一个独立的话题。实际上任何模型都依赖于良好的特征组合来表示整个物理图像的本质。
7. 查bad case
查case的主要作用在于由于多种内外部因素的存在导致我们获得用于训练的数据与预期的大相径庭
重要的事情说三遍!
**数据清洗!**数据清洗!数据清洗!
除了对特征本身的探究之外,在数据清洗的过程中其作用往往令人惊叹但也常常被忽视。
当然数据清洗也是结合对业务和feature的理解来进行的
8.其他trick
◆ 分析特征变量的分布
特征变量被视为连续型数据:当呈现长尾分布形态时,在构建线性模型时考虑使用幂次转换或对数转换。
特征变量属于离散型:建议分析各离散值出现频率的变化情况,并对出现频率较低的指标进行统一编码处理为'其他'类别
将那些不频繁出现的特征编码为一个独特的标记,并且由于这些特征仅携带了微乎其微的信息量,在模型中难以提取出有用的信息。实际上它们在一定程度上容易受到干扰或噪声的影响。
如果直接将数值型数据转化为分类型数据会导致生成后的特征维度过高,则对数值较大的属性(即原始值超过2)进行以下处理:令v = floor(log(v)^2)。
3、归一化处理。对于每一个样本来说,在计算其l2范数之后将该样本中的每一个元素分别除以其对应的范数值作为新的元素值。经过这样的变换后得到的新样本集满足所有新样本的l2-norm均为1。
4、基于Sparse的特征(静态特征)建模,还是Dense的特征(动态特征)建模。
具体来说,在某个特定条件下触发时(即当某个特征被触发时),不再使用固定值1(即不再直接使用该值),而是采用该特征在历史一段时间内(或多个时间窗口)的点击率作为其对应的数值(即作为其取值依据)。一旦某一特定特征被激活后(即当该特定条件触发时),通过实时捕捉动态变化信息(即实时获取变化信息),模型无需频繁更新参数(即不需要频繁地进行参数更新)。因此这种方法在实现上更为简洁且高效。我们曾进行过类似实验验证:当我们采用动态点击率加上离线模型的方法与静态点击率加上在线模型的方法进行对比时(即比较两种不同的方法),两者的收益表现相当良好。
时间被视为连续值,在许多情况下也需要将其划分为区间段,在这种情况下通常会根据语义进行分类。例如早上、中午和晚上这样的切分方式是比较常见的做法。实际上,在划分时间段时也允许存在重叠的情况发生。例如5:00至9:00被认为是早期清晨(early morning),而从8:00至11:00被认为是早晨时间段(morning),则8:00至9:00的时间会同时归于这两个区间。
将具有高势的分类特征通过经验贝叶斯方法转换为数值型特征。
那么High Categorical类别的特征是什么?
例如邮政编码,在拥有100个以上城市时会有数百个邮政编码;许多房子位于同一邮政编码对应的区域。
显然随着邮编数量显著增加单纯的one-hot编码难以达到理想效果。
因此有人采用统计学思想即经验贝叶斯方法对这些类别数据进行映射处理 通过该方法映射后的结果形成了数值型数据。
在最近的一篇论文中 作者详细介绍了相关算法 在实验中发现该方法显著提高了性能。
那么哪些数据适合应用这一技术呢 它们必须满足以下两个条件:
首先类别值之间存在重复;其次基于相同值分组后会产生超过一定阈值(例如100)的不同子群。
通常此类别特征包括building_id manager_id street_address和display_address等 在实际应用中需根据实验结果来确定具体采用哪些变量。
借鉴 Word2Vec 的方法, 将每个类别特征的取值映射到一个连续的向量空间, 并对该向量进行初始化, 与模型协同训练. 经过训练后可同时获得每个 ID 对应的嵌入表示. 具体而言, 可以参考 Rossmann 销量预估竞赛第三名的获奖方案[1], https://github.com/entron/entity-embedding-rossmann. 这种做法在神经网络领域中是一种常见的技术手段, 即将分类变量转化为嵌入形式. 比如说, 如果你有一个包含十万不同 site 的数据集, 将其投影到 64 维或 128 维的空间中, 相当于将原本需要管理十万自由参数的任务简化为只需管理 64 或 128 维的空间. 这里的关键在于原始的空间是稀疏的, 而低维空间能够更好地捕捉数据中的潜在结构. 这种方法的好处在于能够显著减少内存占用需求的同时提高模型预测精度.
Hash编码与Embedding之间的区别主要体现在其学习机制上。具体而言, Embedding通过深度学习模型的学习过程,能够映射至特定的嵌入空间,从而实现对数据特征的有效表示.相比之下, Hash编码采用预先设定好的哈希函数来进行数据映射,并不涉及复杂的参数优化过程.从逻辑层面来看,两者的处理机制存在显著差异:Hash编码允许在相同维度下共享相同的权重参数,从而实现不同样本之间的相似性度量;而Embedding则是一种基于分布表示的方法,能够捕捉到数据语义层次上的细微差别.具体而言,对于两个具有相似语义的对象(如巴西与智利),Embedding会将其映射至高维空间中的特定位置;而Hash编码则会将它们分配到同一个桶中.这种差异不仅体现在算法实现层面,还表现在其在实际应用中的表现形式上:在分类任务中,Hash编码由于其紧凑的空间特性往往表现出更强的计算效率;而Embedding方法则能够更好地适应复杂的非线性关系建模需求.
