前言

自用笔记,目前在看《Learning Data Mining with python 2nd Edition》。

在图书馆发现这本书(第一版译本),顿时就吸引了我的注意力,之前学校也开过《数据挖掘》的课,蛮有意思的,也就纯理论相关,一直没实践。

然后通过书上给的源代码链接,发现这本书今年4月份出了第二版,就下了电子版来研究了。

Getting Started with Data Mining

affinity analysis

第一个例子是关于affinity analysis,给出历史订单,当X和Y同时购买时,可以找出如下规则:

当用户买了X(premise),有多大可能性买Y(conclusion)。

  • 支持度(support): 历史订单中出现premise->conclusion的个数
  • 置信度(confidence):支持度/历史订单中出现premise的个数

最后可以根据置信度从大到小排序,从而帮助我们做出决策。

实现OneR算法

第二个例子是分类问题,通过scikit-learn库的数据集IRIS(花的数据集,有3种类别)来介绍OneR(One Rule)算法,也就是通过选择一个最好的特征来判断类别。

该数据集有150个样本,4个特征,以及每个样本对应的类别。首先对各个特征值进行离散化,书上是通过各个特征值的均值来作为阈值,大于均值为1,否则为0,这样各个特征值只有2种数值了。

然后实现OneR算法:

  • 依次遍历每个特征
    • 遍历特征的每个值(train_feature_value)
      • 根据所有样本中的特征为该值找出最频繁的类
      • 计算错误的样本(不属于最频繁的类)个数
    • 计算该特征总的错误个数
  • 使用错误个数最少的特征来分类
# X样本, y_true样本对应的类别,feature选择的特征,value特征的值
def train_feature_value(X, y_true, feature, value):
    class_count = defaultdict(int)
    for sample, cls in zip(X, y_true):
        if sample[feature] == value:
            class_count[cls] += 1
    most_frequent_class = sorted(class_count.items(), key=itemgetter(1), reverse=True)[0][0]
    error = sum([cnt for cls, cnt in class_count.items() if cls != most_frequent_class])
    return most_frequent_class, error

def train(X, y_true, feature):
    n_samples, n_features = X.shape
    values = set(X[:, feature])
    predictors = {}
    errors = []
    for current_value in values:
        most_frequent_class, error = train_feature_value(X, y_true, feature, current_value)
        predictors[current_value] = most_frequent_class
        errors.append(error)
    total_error = sum(errors)
    return predictors, total_error

def OneR(X, y_true):
    all_predictors = {}
    errors = {}
    for feature in range(X.shape[1]):
        predictor, total_error = train(X, y_true, feature)
        all_predictors[feature] = predictor
        errors[feature] = total_error
    feature = sorted(errors.items(), key=itemgetter(1))[0][0]
    return {'feature': feature, 'predictor': all_predictors[feature]}

解下来就是测试算法了,需要将数据集划分为两个部分:训练集测试集,以防出现overfitting问题,也就是说训练的模型对训练集能进行很好的分类,但是遇到新的样本效果就会很差了。

sklearn.cross_validation(cross validation简称CV)的train_test_split来划分数据集,默认随机抽取75%样本作为训练集,剩下的为测试集:

from sklearn.cross_validation import train_test_split
Xd_train, Xd_test, y_train, y_test = train_test_split(X_d, Y)

测试精度,65.8%,还行:

def predict(X_test, model):
    var = model['feature']
    predictor = model['predictor']
    y_predicted = np.array([predictor[int(sample[var])] for sample in X_test])
    return y_predicted

In [371]: np.mean(predict(Xd_test, model) ==  y_test)
Out[371]: 0.65789473684210531

scikit-learnEstimators分类

scikit-learn库与很多数据挖掘算法,这一章介绍如何调用它来运行一些数据挖掘算法(分类)。

有3个概念需要注意:

  • Estimators: 用于分类、聚类、回归
  • Transformers: 用于预处理、选择数据
  • Pipelines: 将工作流程整合到一块复用

Estimators

Estimators有2个比较重要的函数:

  • fit(): 执行训练用算法,设置内部参数。有2个输入:训练集和对应的类型(就像前面提到的OneR(X, y_true)
  • predict():只有一个测试集输入,输出预测的分类。(就像前面提到的predict(X_test, model)

Nearest neighbors算法

这个算法根据样本周围最近的几个邻居的计数(复杂点的有权重)来判断类型。

由于需要计算每个样本的距离,无疑增加了计算量,有很多优化的算法,比如使用树来进行距离计算,这些算法相当复杂,好在scikit-learn库实现了这些算法。

对于categorical-based数据集效果比较差,可能需要预处理。

距离度量

因为涉及到最近,那么需要距离来度量。书上介绍了3种度量的方法:

  • Euclidean距离,也就是每个特征平方差的和的平方根(当特征比较多的时候,各个样本可能都差不多近。。)
  • Manhattan距离,每个特征的绝对差的和(当特征比较大的时候,可能会无视那些比较小的特征值了,可以通过正规化来解决)
  • Cosine距离,特征向量的夹角,丢失了长度信息(由于不考虑向量长度,适合多个特征的情况。比如可以用于文本挖掘)

读取数据集

从这开始,书上介绍了一个利用Estimators进行训练的数据集。数据集(http://archive.ics.uci.edu/ml/machine-learning-databases/ionosphere/)为csv格式,共有351个样本,每个34个特征,2个分类:good和bad。每行前34个为特征,最后一个为分类。

然后读到X,Y中:

X = np.zeros((351, 34), dtype='float')
y = np.zeros((351,), dtype='bool')
with open('../data/ionosphere.data', 'r') as f:
    reader = csv.reader(f)
    for i, row in enumerate(reader):
        data = [float(dat) for dat in row[:-1]]
        X[i] = data
        Y[i] = row[-1] == 'g'

标准工作流程

首先划分数据集,分为训练集和测试集。

在开始之前,我来测试一下之前的OneR算法精度:

Xd = (X > X.mean(axis=0)).astype(int)
Xd_train, Xd_test, Y_train, Y_test = train_test_split(Xd, Y)
model = OneR(Xd_train, Y_train)
In [590]: (predict(Xd_test, model) == Y_test).mean()
Out[590]: 0.76136363636363635

精度76.14%,还不错啊。。

现在来试试estimators的精度,同样地划分数据集。

from sklearn.neighbors import KNeighborsClassifier
X_train, X_test, Y_train, Y_test = train_test_split(X, Y)
estimator = KNeighborsClassifier()

开始训练

In [593]: estimator.fit(X_train, Y_train)
Out[593]:
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
           metric_params=None, n_jobs=1, n_neighbors=5, p=2,
           weights='uniform')

测试精度:

In [594]: (estimator.predict(X_test) == Y_test).mean()
Out[594]: 0.86363636363636365

86.37%,精度明显提高了。还是默认参数,要是自己对应用比较熟悉的话,调参大法好。后面章节还会介绍参数搜索。(parameter search)

当然精度提高的代价伴随着运算量的增加。

Cross-fold validation(CV)

若数据只划分成一组训练集和测试集,那么难免会出现在幸运的情况下结果比较好。

若将数据划分成多组(fold)训练集和测试集的话,训练每一组求出其精度,在求出平均精度,能够较好的反映出算法的效果。

cross-fold validation框架很好地解决这个问题,过程如下:

  1. 将整个数据集划分成K个部分(folds),基本等大
  2. 每部分,执行如下步骤
    1. 将该部分作为测试集
    2. 剩余(K-1)部分作为训练集
    3. 评估当前测试集的精度
  3. 得出每部分的精度,求出均值

具体如下,先来看看OneR的效果,

from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.cross_validation import cross_val_score

class OneRES(BaseEstimator, ClassifierMixin):
    def __init__(self):
        self.model = {}
    def fit(self, X, Y):
        Xd = (X >= X.mean(axis=0)).astype(int)
        self.model = OneR(Xd, Y)
    def predict(self, X):
        Xd = (X >= X.mean(axis=0)).astype(int)
        return predict(Xd, self.model)

scores = cross_val_score(estimator, X, Y, scoring='accuracy')
In [669]: scores
Out[669]: array([ 0.66666667,  0.65811966,  0.68376068])
In [670]: scores.mean()
Out[670]: 0.66951566951566954

精度66.95%,效果很差了。

再来看看estimator的精度:

from sklearn.cross_validation import cross_val_score
scores = cross_val_score(estimator, X, Y, scoring='accuracy')
In [623]: scores
Out[623]: array([ 0.82051282,  0.78632479,  0.86324786])
In [624]: scores.mean()
Out[624]: 0.8233618233618234

得出平均精度82.34%,很高了。

调参

因为参数可以任意调整,那么当针对特定应用使用特定的参数的效果比使用随意的参数要好。

就拿这个Nearest neighbors算法来说,影响最大的参数应该是被预测样本的邻居数了,邻居过多或过少效果都不好,一般取5到10个。

在scikit-learn中,邻居参数名为n_neighbors,可以通过设置为一个范围的值,来观察参数对结果的影响。

from matplotlib import pyplot as plt
avg_score = []
para_values = list(range(1, 25 + 1))
for n_neighbors in para_values:
    estimator = KNeighborsClassifier(n_neighbors=n_neighbors)
    scores = cross_val_score(estimator, X, Y, scoring='accuracy')
    avg_score.append(scores.mean())

plt.plot(para_values, avg_score, '-o')
plt.xlabel('n_neighbors')
plt.ylabel('accuracy')

如图,显然随着邻居增加的情况下,精度越来越低了。在2个邻居的情况下效果比较好:

Nearest neighbors

预处理

因为有些特征的值比较大,会影响到其他特征。

X_broken = X.copy()
X_broken[:, ::2] /= 10
estimator = KNeighborsClassifier()
In [732]: cross_val_score(estimator, X, Y, scoring='accuracy')
Out[732]: array([ 0.82051282,  0.78632479,  0.86324786])
In [733]: cross_val_score(estimator, X_broken, Y, scoring='accuracy')
Out[733]: array([ 0.75213675,  0.64957265,  0.74358974])

所以如果将所有特征缩小到0-1范围内的值,就能很好解决这个问题。

标准预处理

将特征正规化,可以用scikit-learnMinMaxScaler类。

from sklearn.preprocessing import MinMaxScaler

默认将特征缩放到0-1范围,若设置为其他值,将线性映射到这个范围内。

进行预处理,使用transform函数,首先需要像分类器那样训练,若直接使用fit_transform则可以合并那些步骤。

X_transformed = MinMaxScaler().fit_transform(X)

放到一起

将前面几个步骤整合起来:

X_transformed = MinMaxScaler().fit_transform(X_broken)
estimator = KNeighborsClassifier()
In [765]: cross_val_score(estimator, X_transformed, Y, scoring='accuracy')
Out[765]: array([ 0.82905983,  0.77777778,  0.86324786])

Pipelines

使用Pipelines可以将几个过程串起来(比如数据预处理、训练),来减少代码复杂度。

from sklearn.pipeline import Pipeline

要求最后一步的结果为一个Estimator,前面步骤为Transformers。在Transformers中,数据被依次改变,前一步的输出作为下一步的输入。

前面的例子,可以分为如下两步:

  • 使用MinMaxScaler来缩放特征(Transformer)
  • 使用KNeighborsClassifier来分类(Estimator)

每步可以用一个元组来描述('name', step),放到列表作为Pipeline的输入:

scaling_pipeline = Pipeline([('scale', MinMaxScaler()),
                             ('predict', KNeighborsClassifier())])
In [776]: cross_val_score(scaling_pipeline, X, Y, scoring='accuracy')
Out[776]: array([ 0.82905983,  0.77777778,  0.86324786])

续…