主要凭借对业务本身的理解和建模来定的。
1 向前贪心选择
特征子集X从空集开始,只要特征使得交叉验证之后的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 遗传算法
首先随机产生一批特征子集,并用适应度函数给这些特征子集评分,然后通过交叉、突变等操作繁殖出下一代的特征子集,并且适应度越高的特征子集被选中参加繁殖的概率越高。这样经过N代的繁殖和优胜劣汰后,种群中就产生了适应度函数值最高的特征子集。适应度函数可以用cv score来定义。
# 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 #不更新参数
构建好的特征由于主要是brainstorming的结果,我们很难准确衡量在现实中哪些特征比较重要,哪些特征不那么重要,
比较重要的特征往往含有更加丰富的信息,需要想办法把这些信息暴露出来让模型,特别是线性的模型,能够更好的学习到。
如果数据量不太大的话Python的scikit-learn包提供了不错的工具计算feature_importance_:
http://scikit-learn.org/stable/modules/generated/sklearn.ensemble.ExtraTreesClassifier.html#sklearn-ensemble-extratreesclassifier
我们开脑洞想出来的特征可能也有一些是作用不那么明显的,冗余的特征可能会降低模型预测的能力。
但是一般来说这个步骤对结果的提升是很有限的,主要还是不断的通过不同的角度和方法弄清楚特征的重要性和特征之间的关系,帮助你理清楚整个问题的物理图像。
sklearn.feature_selection里面有很多方法可供参考
RFECV方法比较暴力,可以试试,但是计算量较大,数据量大的时候慎用。
它的基本思想是,对于一个数量为d的feature的集合,他的所有的子集的个数是2的d次方减1(包含空集)。指定一个外部的学习算法,通过该算法计算所有子集的validation error。选择error最小的那个子集作为所挑选的特征。
主要是暴露重要特征中的信息:
针对feature importance比较高的feature做离散化,通过增加feature的数量来逼近可能本身是非线性的模型。
DNN,GBDT等模型在提取非线性特征方面有一定优势
However, according to deep learning theories, when a function can be compactly represented by a deep architecture, it might need a very large architecture to be represented by an insufficiently deep one. So it's still worthwhile to pay attention to these deep learning models.
1.Hashing Trick
具体做法:
用Hash函数把feature都hash到一个定长的hash table,再进行one-hot,这样feature的维度就是固定的了。这中间会有collision,但是有些时候collision可以看作某种程度上的降维,还可以提升效果。
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)])
包括对离散feature做笛卡尔积,或者对连续feature使用联合分段(joint binning),比如使用k-d tree。
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
2 groupby:将数据按照组合的特征id进行分组,使用组的ids作为新特征的值
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 importance等加深了对业务和数据的理解,
个人认为尝试对已有的feature进行分类是个不错的方法,不同的分类维度可以帮你理清思路,帮助你发现有些信息可能在之前的feature中是没有充分暴露出来的,或者feature本身的表示是不准确的。
对着feature一个一个的问自己这个代表的是什么意思,是怎么计算得到的,这个计算的方法是否合理,对结果的影响可能是怎么样的,可以怎样分类,可能跟其他feature有什么关系
不同的feature可以组合成为新的特征,但是这同样依赖于特征分析和很多的实验
如果发现某些信息在现有的特征组合中没有很好的体现出来,那就需要增加新的相关特征。
就特征工程来说这是另外一个话题,事实上任何模型都依赖很好的特征组合来表示整个物理图像。
查case的作用在于因为各种各样内部外部的原因,我们得到的用于训练的数据很可能跟我们想象的或者想要的是不太一样的
重要的事情说三遍!
数据清洗!数据清洗!数据清洗!
除了对特征本身的挖掘,数据清洗的作用往往是非常惊人,但是又容易被忽略的。
当然数据清洗也是结合对业务和feature的理解来进行的
◆ 分析特征变量的分布
◇ 特征变量为连续值:如果为长尾分布并且考虑使用线性模型,可以对变量进行幂变换或者对数变换。
◇ 特征变量为离散值:观察每个离散值的频率分布,对于频次较低的特征,可以考虑统一编码为“其他”类别。
1、将不频繁的特征映射为一个特殊的标签,因为这些特征只包含了非常少的信息,模型提取这些信息很困难,事实上这些特征非常有扰动性。
2、如果直接将数值特征转换为类别特征,生成的维数太大,为了降低生成特征维度,对于特征值大于2的进行如下转换:v <- floor(log(v)^2)。
3、正则化。对每个样本计算其l2-范数,然后对该样本中每个元素除以该范数,这样处理的结果是使得每个处理后样本的l2-norm等于1。
4、基于Sparse的特征(静态特征)建模,还是Dense的特征(动态特征)建模。
简单说就是当某个特征被触发时,不再用1,而是用这个特征历史上一段时间(或者多个时间窗口)的点击率作为其特征取值。当特征动起来以后,通过特征侧捕捉动态信号,模型就不用快速更新了,可以说dense建模的方案相对更加简单优雅。曾经做过这样的实验,当采用动态特征加Offline模型,和静态特征加Online模型,两者收益是相当的。
5、时间是连续值,很多时候也要进行分段,有时候会有一定语义的分法,比如早上,中午,晚上这样的切分。实际上对切分本身来说也可以做成有重叠的, 比如说5点到9点是early morning,8点到11点是morning,这样8点到9点就同时属于两个bin,这也是可以的。
6、高势集类别(High Categorical)进行经验贝叶斯转换成数值feature
什么是High Categorical的特征呢?一个简单的例子就是邮编,有100个城市就会有好几百个邮编,有些房子坐落在同一个邮编下面。很显然随着邮编的数量增多,如果用简单的one-hot编码显然效果不太好,因此有人就用一些统计学思想(经验贝叶斯)将这些类别数据进行一个map,得到的结果是数值数据。在这场比赛中有人分享了一篇paper里面就提到了具体的算法。详细就不仔细讲了,用了这个encoding之后,的确效果提升了很多。那么这场比赛中哪些数据可以进行这样的encoding呢,只要满足下面几点:1. 会重复,2. 根据相同的值分组会分出超过一定数量(比如100)的组。也就是说building_id, manager_id, street_address, display_address都能进行这样的encoding,而取舍就由最后的实验来决定了。
7、参考 word2vec 的方式,将每个类别特征的取值映射到一个连续的向量,对这个向量进行初始化,跟模型一起训练。训练结束后,可以同时得到每个ID的Embedding。具体的使用方式,可以参考 Rossmann 销量预估竞赛第三名的获奖方案,https://github.com/entron/entity-embedding-rossmann。这是在神经网络中常见的做法,就是把分类变量转换为嵌入式变量,做Embedding。比如说你有十万个不同sites,把它投影到64维或者128维的vector上面,相当于原来需要十万个Free parameters,现在只需要64维或128维。之所以能这样做,原来的十万维空间是非常稀疏的,64维或者128维是更稠密的表达,所以还是有非常强的表达意义,好处就是内存需求会更少,相对来说精度也会更高。
Hash和Embedding的区别,Embedding本身是需要学习出来的,比如说id1它投影到怎样的Embedding空间,通过学习来获得。而哈希是通过预定义的哈希函数,直接投影过去,本身的哈希函数是不需要学习的。这里它最基础的逻辑是不一样的,Hash Encoding也就是说你这两维特征可以share相同的weight。比如说巴西和智利放在同一列中,他们有相同的权重,而Embedding实际上是一种distributional的表达方式。它的意思是说巴西可能使用64维上不同取值来代表,智利也是同样用这64维,所以这64维,每一列都参与表达不同的国家,每一个国家都由这64维来表达。它们两个的基本思路上是有所区别的。