pokemonlei

陈磊的博客 | pokemonlei

python与机器学习入门二

写在开头参考

机器学习算法分类:

  • 监督学习(预测):有特征值和目标值,有标准答案
    • 分类:对应数据类型为离散型
      • k-近邻算法
      • 贝叶斯分类
      • 决策树与随机森林
      • 逻辑回归
      • 神经网络
    • 回归:对应数据类型为连续型
      • 线性回归
      • 岭回归
    • 标注
      • 隐马尔科夫模型
  • 无监督学习:只有特征值
    • 聚类
      • k-means

特征选择的方式:

  • 过滤式:
    • 低方差特征
  • 嵌入式:
    • 正则化
    • 决策树
    • 神经网络

精确率与召回率

先熟悉一个概念:混淆矩阵
在分类任务下,预测结果与正确标记之间存在四种不同的组合,构成混淆矩阵(适用于多分类),构成如下:

而精确率与召回率都是通过这个矩阵来算的:

其他标准:
F1-score,反映了模型的稳健性,是一个综合评判标准,公式如下:

分类评估模型API:

sklearn.metrics.classification_report(y_true,y_pred,target_name=None)

  • y_true:真实目标值
  • y_pred:估计器预测的目标值
  • target_name:目标类别名称
  • return:每个类别精确率与召回率,F1,support(预测的数量)

模型的选择与调优

1.交叉验证:为了让被评估的模型更加准确可信

  • 1.将所有训练集数据分成N等分,其中一份作为验证集,其余的当作训练集,然后可以得出一个准确率
  • 2.把另一份作为验证集,其余的作为训练集,再得出一个准确率
  • 3.一直重复,直到所有数据都作了一次验证集,而此时可以得到N个准确率,将N个准确率求平均值。
    分为几等分则成为 几折交叉验证

2.网格搜索(超参数搜索):调参数,如k-近邻超参数K的调整
通常情况下,有很多参数是需要手动指定的,如k-近邻算法中的k值,这种叫超参数。但是手动过程繁琐,所以需要对模型预设几种超参数组合。每组超参数都采用交叉验证来进行评估。最后选出最优参数组合建立模型。

API:sklearn.model_selection.GridSearchCV(estimator,param_grid=None,cv=None)

  • estimator:估计器对象,如实例化的KNN估计器knn = KNeighborsClassifier()
  • param_grid:估计器的参数(dict),如knn算法中指定的邻近多少个 {“n_neighbors”:{1,3,5}}
  • cv:指定几折交叉验证

返回的对象可以执行

  • fit:输入训练数据
  • score:准确率
  • best_score_ : 在交叉验证中的最好结果
  • best_estimator : 最好的参数模型
  • cv_results_ :每次交叉验证后的测试集准确率结果和训练集准确率结果

过拟合与欠拟合

问题:训练数据训练的很好,误差也很小,为什么在测试集上有问题呢?

机器学习中一个重要的话题便是模型的泛化能力,泛化能力强的模型才是好模型,对于训练好的模型,若在训练集表现差,不必说在测试集表现同样会很差,这可能是欠拟合导致;若模型在训练集表现非常好,却在测试集上差强人意,则这便是过拟合导致的,过拟合与欠拟合也可以用 Bias 与 Variance 的角度来解释,欠拟合会导致高 Bias ,过拟合会导致高 Variance ,所以模型需要在 Bias 与 Variance 之间做出一个权衡。

使用简单的模型去拟合复杂数据时,会导致模型很难拟合数据的真实分布,这时模型便欠拟合了,或者说有很大的 Bias,Bias 即为模型的期望输出与其真实输出之间的差异;有时为了得到比较精确的模型而过度拟合训练数据,或者模型复杂度过高时,可能连训练数据的噪音也拟合了,导致模型在训练集上效果非常好,但泛化性能却很差,这时模型便过拟合了,或者说有很大的 Variance,这时模型在不同训练集上得到的模型波动比较大,Variance 刻画了不同训练集得到的模型的输出与这些模型期望输出的差异

欠拟合原因及解决办法:

  • 原因:学习到的数据特征特少
  • 解决办法:增加数据的特征数量

过拟合的原因及解决办法:

  • 原因:
    • 原始特征过多,存在一些嘈杂特征
  • 解决办法:可通过交叉验证检查是否过拟合
    • 进行特征选择,消除关联性大的特征(很难做)
    • 正则化

L2正则化:
作用:可以使得回归系数w的每个元素都很小,都接近0
优点:越小的参数说明模型越简单,越简单的模型则越不容易产生过拟合

更详细的如何分辨是过拟合还是欠拟合以及如何防止,参考文章:https://zhuanlan.zhihu.com/p/29707029

模型的保存和加载

from sklearn.externals import joblib
scikit-learn中的文件保存格式是 pkl
保存:
joblib.dump(rf,’test.pkl’)

  • rf为fit训练好的估计器模型实例

加载:
estimator = joblib.load(‘test.pkl’)

损失函数

损失函数(loss function)是用来估量你模型的预测值f(x)与真实值Y的不一致程度,它是一个非负实值函数,通常使用L(Y, f(x))来表示,损失函数越小,模型的鲁棒性就越好。损失函数是经验风险函数的核心部分,也是结构风险函数重要组成部分。

k-近邻算法(KNN)

####原理与优缺点
KNN是通过测量不同特征值之间的距离进行分类。它的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别,其中K通常是不大于20的整数。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。

KNN算法的结果很大程度取决于K的选择,如下图,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?如果K=3,由于红色三角形所占比例为2/3,绿色圆将被赋予红色三角形那个类,如果K=5,由于蓝色四方形比例为3/5,因此绿色圆被赋予蓝色四方形类。

公式:在KNN中,通过计算对象间距离来作为各个对象之间的非相似性指标,避免了对象之间的匹配问题,在这里距离一般使用欧氏距离或曼哈顿距离:

k-近邻算法需要做标准化处理

优点:

  • 简单,易于理解,无需参数估计,无需训练只需要一次计算就可以得出结果
  • 对异常值不敏感
  • 适合对稀有事件进行分类
  • 可以处理多分类问题

缺点:

  • 对测试样本分类时的计算量大,内存开销大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本;
  • 可解释性差,无法告诉你哪个变量更重要,无法给出决策树那样的规则;
  • K值的选择:k太小,容易受异常点影响,k取值太大,容易受K值数量(类别)波动。可以采用权值的方法(和该样本距离小的邻居权值大)来改进;
  • KNN是一种消极学习方法、懒惰算法。

示例

样本数据:

电影名称 打斗镜头 接吻镜头 电影类型
California Man 3 104 爱情片
He’s Not Really into Dudes 2 100 爱情片
Beautiful woman 1 81 爱情片
Kevin Longblade 101 10 动作片
Robo Slayer 3000 99 5 动作片
Amped II 98 22 动作片
18 90 未知

如果我们通过公式(如欧氏距离)计算出已知电影与未知电影的距离如下:

电影名称 与未知电影的距离
California Man 20.5
He’s Not Really into Dudes 18.7
Beautiful woman 19.2
Kevin Longblade 115.3
Robo Slayer 3000 117.4
Amped II 118.9

按照距离递增排序,可以找到k个距离最近的电影。假定k=3,则三个最靠近的电影依次是:

  • He’s Not Really into Dudes
  • Beautiful woman
  • California Man
    kNN按照距离最近的三部电影的类型,决定未知电影的类型——爱情片

API与实战demo

API:sklearn.neighbors.KNeighborsClassifier(n_neighbors=5,algorithm=’auto’)

  • n_neighbors:int,可选,默认为5,设置查询默认使用的邻居数
  • algorithm:{‘auto’,’ball_tree’,’kd_tree’,’brute’},可选用计算最近邻居的算法,’ball_tree’将会使用BallTree,’kd_tree’会使用KDTree,auto根据传递给fit的值来自动决定

示例:facebook题目:k近邻算法预测入住位置

数据集介绍:
本次实验的数据集来自于Kaggle与Facebook合作的机器学习竞赛,旨在通过由facebook提供的2000多万条数据信息预测一个人想要登记入住的地方。
数据来源:https://www.kaggle.com/c/facebook-v-predicting-check-ins/data
其中:
train.csv

  • row_id:登记事件的ID
  • xy:坐标
  • accuracy:定位准确性
  • time:时间戳
  • place_id:业务的ID,也是预测的目标值

分析思路:

  • 数据集预处理
    • 数据量太 → 缩小坐标范围
    • 时间数据 → 转化为年月日(增加新的时间特征,便于后续计算)
    • 类别太多 → 按照指定条件进行转换
  • 分割数据及标准化
    • 选定”place_id”为目标值,数据集预处理结果为目标值。
    • 使用StandardScaler类对数据进行标准化处理。
  • KNN分类预测
    • 在分类预测过程中,使用超参数搜索API对模型进行选择和调优。

数据的原始内容为:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV
import pandas as pd


def knncls():
'''
k-近邻算法预测用户入住位置
:return: None
'''
# 读取数据
data = pd.read_csv("./train.csv")
# print(data.head(10))

# 特征工程
# 1.通过设定xy范围缩小数据
data = data.query("x > 1.0 & x < 1.25 & y > 2.5 & y < 2.75") # query相当于一个查询语句,参数为查询条件

# 2.处理时间戳数据
time_value = pd.to_datetime(data['time'], unit='s') # 将时间戳转换为年-月-日 时-分-秒格式
# print(time_value)
'''
timevalue的值为:
0 1970-01-06 10:45:02
1 1970-01-03 03:49:15
2 1970-01-04 17:37:28
3 1970-01-09 03:43:07
'''
# 把日期转换为字典格式
time_value = pd.DatetimeIndex(time_value)
# 3.添加构造一些特征
data['day'] = time_value.day
data['hour'] = time_value.hour
data['weekday'] = time_value.weekday
# 4.把时间戳特征删除,sklearn中axis为1表示列
data = data.drop(['time'], axis=1)

# 5.只保留入住人数大于3的目标位置,生成新的data
place_count = data.groupby("place_id").count() # 按照'place_id'进行分组,然后统计次数。groupby使用参考:https://blog.csdn.net/m0_37870649/article/details/80979809
# print(place_count)
'''
此时print的place_count如下:
此时place_id为索引,而原来的特征值都变为了对应索引的place_id出现了多少次
place_id row_id x y accuracy day hour weekday
1000015801 78 78 78 78 78 78 78
1000017288 95 95 95 95 95 95 95
1000025138 563 563 563 563 563 563 563
1000052096 961 961 961 961 961 961 961
'''
place_count_r = place_count[
place_count.row_id > 3].reset_index() # 筛选place_count中row_id大于3的,然后通过reset_index()把place_id重新设为特征而不是索引
train_data = data[data["place_id"].isin(place_count_r["place_id"])] #通过对比筛选

# 提取特征值和目标值
x = train_data.drop(["place_id", "row_id"], axis=1)

y = train_data["place_id"]

# 分割数据集
#参数:特征值,目标值,测试集比例。返回值依次为:训练集特征值、测试集特征值,训练集目标值,测试集目标值
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.25)

# 对训练集和测试集的特征值进行标准化
std = StandardScaler()

x_train = std.fit_transform(x_train)
x_test = std.transform(x_test)

# 实例化knn估计器
knn = KNeighborsClassifier() # n_neighbors,默认5

#knn.fit(x_train, y_train) #参数:训练集特征值、训练集目标值
# 预测结果
#y_predict = knn.predict(x_test) #参数:测试集
# 打印准确率
#print("准确率为:", knn.score(x_test, y_test))
'''
准确率为: 0.4732860520094563
'''

# 进阶思考:
# 交叉验证与网格搜索对K-近邻算法调优
param = {"n_neighbors": [i * 2 for i in range(1, 5)]}
gc = GridSearchCV(knn, param_grid=param, cv=5)
gc.fit(x_train, y_train)

print("在测试集中的准确率:", gc.score(x_test, y_test))
print("在交叉验证中验证集的最好结果:", gc.best_score_)
print("使用的最好的模型:", gc.best_estimator_)
'''
在测试集中的准确率: 0.47848699763593383
在交叉验证中验证集的最好结果: 0.47540983606557374
使用的最好的模型: KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=None, n_neighbors=8, p=2,
weights='uniform')
'''
return None
if __name__ == "__main__":
knncls()

朴素贝叶斯算法

使用前提:特征之间独立,不互相影响

该算法是有监督的学习算法,解决的是分类问题,如客户是否流失、是否值得投资、信用等级评定等多分类问题

原理

条件概率中: P(A|B)表示事件B已经发生的前提下,事件A发生的概率,叫做事件B发生下事件A的条件概率。其基本求解公式为:P(A|B) = P(AB)/P(B)。
通常我们可以很容易直接得出P(A|B),P(B|A)则很难直接得出,但我们更关心P(B|A),贝叶斯定理就为我们打通从P(A|B)获得P(B|A)的道路。
贝叶斯公式如下:

其中:

  • P(类别)指每个文档类别的概率,计算方式 (某文档类别数 / 总文档数量)
  • P(特征|类别) = 指给定类别下特征的概率,计算方式:P(F1|C) = Ni/N
    • Ni为该F1特征词在C类别所有文档中出现的次数
    • N为所属类别C下的文档所有词出现的次数和
  • 在比较一个文章属于不同文章类型的概率时,会发现分母P(特征)是相同的,所以可以省略掉

拉普拉斯平滑:
如果词频列表里面有很多出现次数都为0的词,在计算的时候可能使计算结果都变为0,这是不合理的,解决方式就是增加拉普拉斯修正

其中,α为指定的系数,一般为1,m为训练文档中统计出的特征词个数。

朴素贝叶斯分类的流程可以由下图表示:

优缺点

优点:

  • 算法逻辑简单,易于实现
  • 分类过程中时空开销小
  • 对缺失数据不太敏感
  • 对小规模的数据表现很好,能处理多分类任务,适合增量式训练,尤其是数据量超出内存时,可以一批批的去增量训练。

缺点:
理论上,朴素贝叶斯模型与其他分类方法相比具有最小的误差率。但是实际上并非总是如此,这是因为朴素贝叶斯模型假设属性之间相互独立,这个假设在实际应用中往往是不成立的,在属性个数比较多或者属性之间相关性较大时,分类效果不好

示例

一个简单示例如下:

可以看到由于云计算在娱乐文章里出现的频率是0,所以导致最后结果是0,这里需要进行拉普拉斯平滑处理。
即将P(影院,支付宝,云计算|娱乐)计算时的 分子+1,分母+4,

API与实战demo

pythonAPI:sklearn.naive_bayes.MultinomialNB(alpha = 1.0)

  • alpha:拉普拉斯平滑系数,默认1

此处的实例用的还是之前内置的sklearn20类新闻分类的数据,20个新闻组数据包含了20个主题的18000个新闻组帖子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

from sklearn.datasets import fetch_20newsgroups
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB


def naviebayes():
'''
朴素贝叶斯对新闻进行分类
:return: None
'''
news = fetch_20newsgroups(subset='all')

# 进行分割
x_train, x_test, y_train, y_test = train_test_split(news.data, news.target, test_size=0.25)

# 对数据集进行特征抽取,用tf-idf方法
tf = TfidfVectorizer()

# 以训练集中的词的列表进行每篇文章重要性统计
x_train = tf.fit_transform(x_train) # 此时x_train为sparse矩阵,大概内容就是记录着:(行,列) 对应数值
print(tf.get_feature_names())
'''
特征名,也就是每个单词
'davidst', 'davidstern', 'davidw', 'davie', 'daviel', 'davies', 'davinci', 'davis', 'davison',
'''

x_test = tf.transform(x_test)

# 进行朴素贝叶斯算法的预测
mlt = MultinomialNB(alpha=1.0)
print(x_train.toarray())
'''
大多数是0,因为一篇文章不是所有的词都有,每一行代表一篇文章,列就是上面的特征名,即单词,行列对应值为TF-IDF值
[[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
..................
[0. 0. 0. ... 0. 0. 0.]]
'''

mlt.fit(x_train, y_train)

y_predict = mlt.predict(x_test)
print("预测的文章类别为:", y_predict)
'''
预测的文章类别为: [ 7 16 0 ... 4 8 1]
'''

# 得出准确率
print("准确率为:", mlt.score(x_test, y_test))
'''
准确率为: 0.8293718166383701
'''
return None


if __name__ == "__main__":
naviebayes()
番外篇,R语言与朴素贝叶斯

R语言中的klaR包提供了朴素贝叶斯算法实现的函数NaiveBayes
NaiveBayes(formula, data, …, subset, na.action= na.pass)
NaiveBayes(x, grouping, prior, usekernel= FALSE, fL = 0, …)

  • formula指定参与模型计算的变量,以公式形式给出,类似于y=x1+x2+x3;
  • data用于指定需要分析的数据对象;
  • na.action指定缺失值的处理方法,默认情况下不将缺失值纳入模型计算,也不会发生报错信息,当设为“na.omit”时则会删除含有缺失值的样本;
  • x指定需要处理的数据,可以是数据框形式,也可以是矩阵形式;
  • grouping为每个观测样本指定所属类别;
  • prior可为各个类别指定先验概率,默认情况下用各个类别的样本比例作为先验概率;
  • usekernel指定密度估计的方法(在无法判断数据的分布时,采用密度密度估计方法),默认情况下使用正态分布密度估计,设为TRUE时,则使用核密度估计方法;
  • fL指定是否进行拉普拉斯修正,默认情况下不对数据进行修正,当数据量较小时,可以设置该参数为1,即进行拉普拉斯修正。

决策树(Decision Tree)和随机森林(Random Forest)

  • 决策树是用树的结构来构建分类模型,每个节点代表着一个属性,根据这个属性的划分,进入这个节点的儿子节点,直至叶子节点,每个叶子节点都表征着一定的类别,从而达到分类的目的。
    • 常用的决策树有ID4,C4.5,CART等。在生成树的过程中,需要选择用那个特征进行剖分,一般来说,选取的原则是,分开后能尽可能地提升纯度,可以用信息增益,增益率,以及基尼系数等指标来衡量。如果是一棵树的话,为了避免过拟合,还要进行剪枝(prunning),取消那些可能会导致验证集误差上升的节点。
  • 随机森林实际上是一种特殊的bagging方法,它将决策树用作bagging中的模型。首先,用bootstrap方法生成m个训练集,然后,对于每个训练集,构造一颗决策树,在节点找特征进行分裂的时候,并不是对所有特征找到能使得指标(如信息增益)最大的,而是在特征中随机抽取一部分特征,在抽到的特征中间找到最优解,应用于节点,进行分裂。随机森林的方法由于有了bagging,也就是集成的思想在,实际上相当于对于样本和特征都进行了采样(如果把训练数据看成矩阵,就像实际中常见的那样,那么就是一个行和列都进行采样的过程),所以可以避免过拟合。

决策树

信息论基础

在信息论中,熵(英语:entropy)是接收的每条消息中包含的信息的平均量,又被称为信息熵、信源熵、平均自信息量。这里,“消息”代表来自分布或数据流中的事件、样本或特征。
熵最好理解为不确定性的量度而不是确定性的量度,因为越随机的信源的熵越大

香农的信息熵理论:
如果n个事件发生的概率分别为p1,p2,p3….pn则他们的准确信息量应该是:
H = -(p1Logp1 + p2logp2 + p3logp3 + … + pnlogpn)
H的专业术语为信息熵,单位为比特,公式:

例如,当有32支球队比赛时,猜测谁是冠军,在没有任何信息的情况下,每个球队获胜的概率都是 1/32,此时信息熵为
H = -(1/32Log1/32 + 1/32log1/32 + 1/32log1/32 + … + 1/32log1/32) = 5
而当我们得到一些信息的情况下,比如不同球队的获胜几率为多少,那我们最终得出的信息熵H将会小于5。

信息增益:
当得知一个特征条件之后,减少的信息熵的大小。

决策树的生成

决策树划分依据

  1. ID3:信息增益最大
    特征A对训练数据集D的信息增益g(D,A),定义为集合D的信息熵H(D)与 特征A给出条件下D的信息条件熵H(D|A) 之差,即公式:
    g(D,A) = H(D) - H(D|A)
    此处信息增益表示,得知特征A的信息而使得类D的信息的不确定性减少的程度,计算H(D)和H(D|A)公式:

上面这公式有点复杂,举个例子:
有如下银行贷款的数据,通过前面的特征来决定是否可以贷款。

对于没有加入特征之前,只看是否贷款数据的信息熵:一共有15条数据,9条可以贷款,6条不可以。所以此时总的信息熵为:0.971,计算方式如下

然后我们让A1,A2,A3,A4分别表示年龄,工作,有自己房子和信贷情况四个特征,则分别计算他们的信息增益即可选择下一次决策用什么特征。
此处以年龄为例,而年龄中青年、中年、老年各占5个,即 1/3,根据公式,年龄的信息增益计算为:
g(D,A1) = H(D) - H(D’|年龄) = 0.971 - [1/3H(青年) + 1/3H(中年)+ 1/3H(老年)],
此时需要计算H(青年)针对类别的信息熵,由于5个青年中,有3个类别是‘否’,2个对应类别是‘是’,所以计算方式如下:
H(青年)= -(2/log2/5 + 3/5log3/5)
中年和老年的计算方式类似,最终结果为:

同理,其他的特征信息增益也可以计算出来,g(D,A2)=0.324,g(D,A3)= 0.420,g(D,A4)= 0.363,相比较A3所对应的有自己的房子这个特征的信息增益最大,所以特征A3为最有特征的一个属性。

  1. C4.5:信息增益比最大
  2. CART:回归树
    • 回归树:平方误差最小
    • 分类树:基尼系数(Gini)最小,基尼系数的划分更加细致

决策树优缺点

优点:

  • 简单的理解和解释,树木可视化
  • 不需要太多的数据准备操作,其他技术通常需要数据归一化等操作

缺点:

  • 决策树可能过于复杂,而且训练集中正确率高不代表测试集中正确率高,有些异常点会导致结果有误。过拟合

改进:

  • 剪枝cart算法
  • 随机森林

经典案例:泰坦尼克号乘幸存预测

决策树分类器API:sklearn.tree.DecisionTreeClassifier(criterion=’gini’,max_depth=None,random_state=None)

  • criterion:默认是‘gini’系数,也可以选择信息增益的熵’entropy’
  • max_depth:树的深度大小
  • random_state:随机数种子
    method:
  • decision_path:返回决策树的路径

决策树的结构,本地保存API:sklearn.tree.export_graphviz(extimator,outfile=”tree.dot”,feature_names=[‘’,’’])

  • extimator:实例化的估计器对象
  • outfile:保存路径和文件,必须是后缀dot格式,这格式课方便的转换为pdf,png等格式
  • feature_names:特征名,可自己指定
  • 用软件graphviz运行命令:dot -Tpng tree.dot -o tree.png 来查看,tree.png改为tree.pdf则转换为pdf

案例数据:http://biostat.mc.vanderbilt.edu/wiki/pub/Main/DataSets/titanic.txt
数据中包含的特征:票的类别,存活结果,乘坐版(pclass),年龄,登陆,home.dest,房间,票,船和性别等。其中乘坐班是指乘客班(1,2,3),是社会经济阶层的代表。其中age数据还存在缺失需要处理。
处理流程:

  • pd读取数据
  • 选择有影响的特征,处理缺失值
  • 进行特征工程,pd转换字典,特征抽取x_train.to_dict(orient=”recored”)
  • 决策树估计器流程

案例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

import pandas as pd
from sklearn.feature_extraction import DictVectorizer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier,export_graphviz

def decision():
'''
决策树对泰坦尼克号乘客进行预测生死
:return:None
'''
# 获取数据
titan = pd.read_csv("http://biostat.mc.vanderbilt.edu/wiki/pub/Main/DataSets/titanic.txt")

# 处理数据,找出特征值和目标值,这里以三个为例
x = titan[['pclass', 'age', 'sex']]
print(x)
'''
pclass age sex
0 1st 29.0000 female
1 1st 2.0000 female
2 1st 30.0000 male
3 1st 25.0000 female
'''
y = titan['survived']

# 缺失值处理
x['age'].fillna(x['age'].mean(), inplace=True)

# 分割数据集到训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.25)

# 进行特征工程,当特征是类别的时候,要进行ont-hot编码
dict = DictVectorizer(sparse=False)

x_train = dict.fit_transform(x_train.to_dict(orient="records"))
print(dict.get_feature_names())
'''
['age', 'pclass=1st', 'pclass=2nd', 'pclass=3rd', 'sex=female', 'sex=male']
'''
print(x_train)
'''
刚好与上面对应
[[29. 1. 0. 0. 1. 0. ]
[ 2. 1. 0. 0. 1. 0. ]
[30. 1. 0. 0. 0. 1. ]
'''
x_test = dict.transform(x_test.to_dict(orient="records"))

# 用决策树估计器进行预测
dec = DecisionTreeClassifier() # max_depth是个超参数,值可以影响结果,也决定了决策树的深度
dec.fit(x_train,y_train)

#预测准确率
print("DecisionTreeClassifier无参数时,预测的准确率为:",dec.score(x_test,y_test))
'''
DecisionTreeClassifier无参数时,预测的准确率为: 0.8206686930091185
'''

#导出决策树的结构,截图在之后给出
export_graphviz(dec,out_file="./tree.dot",feature_names=['age', 'pclass=1st', 'pclass=2nd', 'pclass=3rd', 'sex=女性', 'sex=男性'])
return None


if __name__ == "__main__":
decision()

上述代码保存后的决策树结构图

随机森林-集成学习方法

集成学习方法

集成学习(Ensemble learning)通过组合几种模型来提高机器学习的效果。与单一模型相比,该方法可以提供更好的预测结果。
集成学习有一个重要的概念叫Diversity,也就是说每个分类器要长得尽量不一样。就像一个团队,有的人擅长策划,有人擅长拉赞助,有人擅长执行,这样才是一个牛逼的团队。集成学习中每个分类器也要有一定差异,这样才是一个好的集成。

集成学习算法主要有Boosting和Bagging两种类型:

  • Boosting:通过迭代地训练一系列的分类器,每个分类器采用的样本的选择方式都和上一轮的学习结果有关。例如在AdaBoost中,之前分类错误的样本有较高的可能性被选到,在之前分类正确的样本有较小的概率被选到。就像你在背单词,你今天总会选择前面几天背不下来的单词,Boosting也会选择前面轮数没学下来的样本。这个类别下面的主要算法是AdaBoost和GBDT。
  • Bagging:每个分类器都随机从原样本中做有放回的采样,然后分别在这些采样后的样本上训练分类器,然后再把这些分类器组合起来。简单的多数投票一般就可以。随机森林就是这个类别的一个经典算法,仔细学习你会发现它的每棵树和基本的决策树的不同,非常神奇的算法。

额外拓展:集成方法是将几种机器学习技术组合成一个预测模型的元算法,以达到减小方差(bagging)、偏差(boosting)或改进预测(stacking)的效果。

随机森林的定义,过程,优势

定义:在机器学习中,随机森林是一个包含多个决策树的分类器,并且其输出的类别是由个别树输出的类别的众数而定。

过程:

  1. 单个树的建立过程:假如有N个样本,M个特征,随机有放回的抽样,即:Bagging:Bootstrap Aggregating
    • 随机在N个样本中有放回的选择一个样本,重复N次,因为有放回所以可能有重复
    • 随机在M个特征当中选出m个特征
      • m的取值:m<<M
  2. 重复1过程,建立多颗决策树,他们的样本和特征大多不一样。

优势:

  • 当前所有算法中,具有极好的准确率
  • 能够有效地运行在大数据集上
  • 能够处理具有高维特征的输入样本,而且不需要降维
  • 能够评估各个特征在分类问题上的重要性

随机森林与泰坦尼克号乘客幸存预测

随机森林API:class sklearn.ensemble.RandomForestClassifier(n_estimators=10, criterion=’gini’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=’auto’, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, bootstrap=True, oob_score=False, n_jobs=1, random_state=None, verbose=0, warm_start=False, class_weight=None)
列举几个重要参数,参数实在太多,超参数也有很多了,需要的时候再搜索好了:

  • n_estimators : integer,可选default=10,森林里(决策)树的数目
  • criterion : string, 可选(默认值为“gini”),衡量分裂质量的性能(函数)。 受支持的标准是基尼不纯度的”gini”,和信息增益的”entropy”(熵)
  • max_features: int, float, string or None,可选default=”auto”,寻找最佳分割时需要考虑的特征数目,不同的值代表不同的考虑方式:这里n_features为总的特征数
    • 如果是int,就要考虑每一次分割处的max_feature特征
    • 如果是float,那么max_features就是一个百分比,那么(max_feature*n_features)特征整数值是在每个分割处考虑的
    • 如果是auto,那么max_features=sqrt(n_features),即n_features的平方根值。
    • 如果是log2,那么max_features=log2(n_features)
    • 如果是None,那么max_features=n_features
  • max_depth : integer or None, 可选的(默认为None),(决策)树的最大深度。如果值为None,那么会扩展节点,直到所有的叶子是纯净的,或者直到所有叶子包含少于min_sample_split的样本。
  • bootstrap : boolean, 可选的(default=True),建立决策树时,是否使用有放回抽样。

代码示例:处理数据与之前的决策树一样,只是把决策树估计器改成了随机森林,然后进行了网格搜索与交叉验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

import pandas as pd
from sklearn.feature_extraction import DictVectorizer
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier

def decision():
'''
决策树对泰坦尼克号乘客进行预测生死
:return:None
'''
# 获取数据
titan = pd.read_csv("http://biostat.mc.vanderbilt.edu/wiki/pub/Main/DataSets/titanic.txt")

# 处理数据,找出特征值和目标值,这里以三个为例
x = titan[['pclass', 'age', 'sex']]
print(x)
'''
pclass age sex
0 1st 29.0000 female
1 1st 2.0000 female
2 1st 30.0000 male
3 1st 25.0000 female
'''
y = titan['survived']

# 缺失值处理
x['age'].fillna(x['age'].mean(), inplace=True)

# 分割数据集到训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.25)

# 进行特征工程,当特征是类别的时候,要进行ont-hot编码
dict = DictVectorizer(sparse=False)

x_train = dict.fit_transform(x_train.to_dict(orient="records"))
print(dict.get_feature_names())
'''
['age', 'pclass=1st', 'pclass=2nd', 'pclass=3rd', 'sex=female', 'sex=male']
'''
print(x_train)
'''
刚好与上面对应
[[29. 1. 0. 0. 1. 0. ]
[ 2. 1. 0. 0. 1. 0. ]
[30. 1. 0. 0. 0. 1. ]
'''
x_test = dict.transform(x_test.to_dict(orient="records"))

# 随机森林估计器进行预测,超参数调优(这里以两个参数为例)
rf = RandomForestClassifier()

#网格搜索与交叉验证寻找最佳参数
param = {"n_estimators":[10,20,100,120,200,300,500,800],"max_depth":[5,8,15,20,30]}
gc = GridSearchCV(rf,param_grid=param,cv=2)
gc.fit(x_train,y_train)
print("准确率为:",gc.score(x_test,y_test))
'''
准确率为: 0.8267477203647416
'''
print("选择的参数模型:",gc.best_params_)
'''
选择的参数模型: {'max_depth': 5, 'n_estimators': 10}
'''
return None


if __name__ == "__main__":
decision()

线性回归,迭代的算法

回归问题:目标值是连续的,如房价,营业额等。

定义与原理

线性关系模型:
一个通过属性的线性组合来进行预测的函数:

w为权重,b称为偏置项,可理解为w0*1

线性回归定义:

在统计学中,线性回归(Linear regression)是利用称为线性回归方程的最小二乘 函数对一个或多个自变量和因变量之间关系进行建模的一种回归分析。这种函数是一个或多个称为回归系数的模型参数的线性组合。只有一个自变量的情况称为简单回归,大于一个自变量情况的叫做多元回归。

通用公式:
x为特征值,w为要求的值

线性回归的损失函数:平方损失函数(最小二乘法, Ordinary Least Squares )

最小二乘法是线性回归的一种,OLS将问题转化成了一个凸优化问题。在线性回归中,它假设样本和噪声都服从高斯分布(为什么假设成高斯分布呢?其实这里隐藏了一个小知识点,就是中心极限定理),最后通过极大似然估计(MLE)可以推导出最小二乘式子。最小二乘的基本原则是:最优拟合直线应该是使各点到回归直线的距离和最小的直线,即平方和最小。换言之,OLS是基于距离的,而这个距离就是我们用的最多的欧几里得距离。为什么它会选择使用欧式距离作为误差度量呢(即Mean squared error, MSE),主要有以下几个原因:

  • 简单,计算方便
  • 欧氏距离是一种很好的相似性度量标准
  • 在不同的表示域变换后特征性质不变

下面的式子,f(x):模型的预测值。Y:真实值
平方损失(Square loss)的标准形式如下:

当样本个数为n时,此时的损失函数变为:

Y-f(X)表示的是残差,整个式子表示的是残差的平方和,而我们的目的就是最小化这个目标函数值(注:该式子未加入正则项),也就是最小化残差的平方和(residual sum of squares,RSS)。

而在实际应用中,通常会使用均方差(MSE)作为一项衡量指标,公式如下:假设有n个数据,后面的减法为 预测值-真实值

我们通常说的线性有两种情况,一种是因变量y是自变量x的线性函数,一种是因变量y是参数α的线性函数。在机器学习中,通常指的都是后一种情况。

两种求解方法

从前面得知,线性回归要求每个特征对应的权重w值,使得线性回归的损失函数值最小,有两种方法:

  1. 最小二乘法之正规方程(不常用)
    X为特征值矩阵,y为目标值矩阵,求解方程如下:

缺点:当特征过于复杂,求解速度太慢

  1. 最小二乘法之梯度下降SGD(理解过程) 线性回归中一直在迭代,刚开始是原始值减去后面的学习速率*方向,得到一个新的值,下次迭代用这个新的值继续减去 学习速率*方向。
    使用:面对训练数据规模十分庞大的任务

正规方程与梯度下降对比:

API与案例

sklearn.linear_model.LinearRegression()

  • 普通最小二乘线性回归
  • coef_:求出的回归系数

sklearn.linear_model.SGDRegressor()

  • 通过使用SGD最小化线性模型
  • coef_:求出的回归系数

均方差算回归损失的API:

sklearn.metrics.mean_squared_error(y_true,y_pred)

  • y_true:真实值
  • y_pred:预测值
  • return:浮点数结果
  • 真实值和预测值为标准化之前的值

案例:波士顿房价预测
流程:

  1. 数据获取
  2. 数据分割
  3. 训练与测试数据标准化处理
  4. 使用最简单的线性回归模型LinearRegression和SGDRegressor对房价进行预测

代码案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

from sklearn.datasets import load_boston
from sklearn.linear_model import LinearRegression, SGDRegressor
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error


def mylinear():
'''
线性回归预测房子价格
:return: None
'''
# 获取数据
lb = load_boston()
# 分割数据集到训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(lb.data, lb.target, test_size=0.25)

# 进行标准化
# 这里特征值和目标值都要进行
std_x = StandardScaler() # 对特征值进行标准化
x_train = std_x.fit_transform(x_train)
x_test = std_x.transform(x_test)

std_y = StandardScaler() # 对目标值进行标准化
y_train = std_y.fit_transform(y_train.reshape(-1, 1)) # 要求是二维
y_test = std_y.fit_transform(y_test.reshape(-1, 1))

# estimator预测

# 正规方程求解方式预测
lr = LinearRegression()
lr.fit(x_train, y_train)
print("回归系数为:", lr.coef_)
''''
回归系数为: [[-0.13482789 0.14684301 0.01549439 0.03522313 -0.21053828 0.32880086
-0.00599198 -0.39363529 0.25896495 -0.2026098 -0.21658332 0.09938895
-0.38005182]]
'''
# 预测测试集的房子价格
y_lr_predict = std_y.inverse_transform(lr.predict(x_test)) # 此时的值还是标准化后的,所以要inverse

print("测试集里每个房子的预测价格:", y_lr_predict)
'''
测试集里每个房子的预测价格: [[-5.23859608]
[39.61950876]
[35.86414606]
......
[20.31808398]
[17.36664138]]
'''
print("正规方程的均方差:", mean_squared_error(std_y.inverse_transform(y_test), y_lr_predict))
'''
正规方程的均方差: 24.203056713653485
'''
# 梯度下降进行房价预测
sgd = SGDRegressor() # 这里可以指定学习率
sgd.fit(x_train, y_train)
print("SGD预测回归系数为:", sgd.coef_)
''''
SGD预测回归系数为: [-0.08189344 0.07032661 -0.03413881 0.10527339 -0.09947185 0.33781714
-0.04008106 -0.21279579 0.06362954 -0.05064323 -0.19061834 0.10570017
-0.36071628]
'''
# 预测测试集的房子价格
y_sgd_predict = std_y.inverse_transform(sgd.predict(x_test)) # 此时的值还是标准化后的,所以要inverse

print("测试集里每个房子的预测价格:", y_sgd_predict)
'''
测试集里每个房子的预测价格:[22.25491159 30.38773853 12.72306638 35.68389966 29.69842243 21.70338892
15.23925266 27.85422604 24.72436984 31.05806439 16.91816538 28.40787834
......
21.3051416 23.92421059 20.83558394 23.80267794 28.17431605 32.1886237
25.89847536]
'''

print("梯度下降的均方差:", mean_squared_error(std_y.inverse_transform(y_test), y_sgd_predict))
'''
梯度下降的均方差: 24.303529166136546
'''
return None


if __name__ == "__main__":
mylinear()

岭回归解决过拟合

线性回归LinearRegression 容易出现过拟合,为了把训练集数据表现更好
而岭回归是带有正则化的线性回归

线性回归与岭回归Ridge对比:
岭回归得到的回归系数更符合实际,更可靠。另外,能让估计参数的波动范围变小,变的稳定。在存在病态数据偏多的研究中有较大的实用价值。

具有L2正则化的线性最小二乘法:sklearn.linear_model.Ridge(alpha=1.0)

  • alpha:正则化力度
  • coef_:求解的回归系数w

上面的案例用岭回归解决:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 岭回归进行房价预测
rd = Ridge(alpha=1.0) # 这里可以指定学习率
rd.fit(x_train, y_train)
print("岭回归预测回归系数为:", rd.coef_)
''''
岭回归预测回归系数为: [[-0.07357443 0.11581584 0.0248411 0.06555144 -0.17895055 0.27716807
-0.01529063 -0.32589572 0.24111193 -0.19075882 -0.21855738 0.09673899
-0.44673051]]
'''
# 预测测试集的房子价格
y_rd_predict = std_y.inverse_transform(rd.predict(x_test)) # 此时的值还是标准化后的,所以要inverse

print("测试集里每个房子的预测价格:", y_rd_predict)
'''
测试集里每个房子的预测价格: [[33.05501377]
[29.49658894]
......
[19.62373227]
[12.41679022]]
'''
print("岭回归的均方差:", mean_squared_error(std_y.inverse_transform(y_test), y_sgd_predict))
'''
岭回归的均方差: 27.097530207872403
'''

逻辑回归

逻辑回归的输入就是线性回归的式子,而且也是自我学习的过程,要迭代回归
逻辑回归可以做什么,适应场景:二分类问题

  • 广告是否会被点击
  • 是否为垃圾邮件
  • 是否患病
  • 金融诈骗

逻辑回归的输入:

过程:如何从线性回归的输入变为逻辑回归的分类

先了解一下什么事sigmoid函数:将输入转换为0~1,与y轴交叉点默认为0.5

  • 横坐标是一个具体的值
  • 输出纵坐标是0~1之间的值,可看做概率值

而sigmoid的公式和逻辑回归的公式如下:

  • e:常数2.71
  • Z:回归的结果

逻辑回归的损失函数:log对数似然损失函数

有些人可能觉得逻辑回归的损失函数就是平方损失,其实并不是。平方损失函数可以通过线性回归在假设样本是高斯分布的条件下推导得到,而逻辑回归得到的并不是平方损失。在逻辑回归的推导中,它假设样本服从伯努利分布(0-1分布),然后求得满足该分布的似然函数,接着取对数求极值等等。而逻辑回归并没有求似然函数的极值,而是把极大化当做是一种思想,进而推导出它的经验风险函数为:最小化负的似然函数(即max F(y, f(x)) —> min -F(y, f(x)))。从损失函数的视角来看,它就成了log损失函数了。

损失函数公式如下:

当目标值是1,此时损失值与预测结果为1的概率(x轴)的关系如下:

当目标值是0,此时损失值与预测结果为0的概率(x轴)的关系如下:

API与案例:

逻辑回归API:sklearn.linear_model.LogisticRegression(penalty=’l2’,C=1.0)

  • penalty:正则化方式
  • c:正则化粒度

案例:预测癌症肿瘤(乳腺癌)
数据地址:http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data
数据内容:

  • 每个案例11列数据,第一列是检索Id,后9列是与病情相关的医学特征,最后1列表示肿瘤类型的数值
  • 肿瘤类型数值:2为良性,占65.5%,4为恶性,占34.5%
    • 注:逻辑回归中,因为针对的是2分类型,所以只有两个结果,只用计算一个概率值来表示属于某一种类型的概率,另一个为(1-概率),而这个概率值一般是数据中占比比较少的那一个类型,比如这里的恶性
  • 其中包含几个缺失值,用“?”标出

流程:

  • 从网上获取数据
  • 数据缺失值处理、标准化
  • 估计器流程

代码案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

from sklearn.metrics import mean_squared_error,classification_report
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
import pandas as pd
import numpy as np

def logistic():
'''
逻辑回归做二分类进行癌症肿瘤是否是恶性的预测(根据属性特征)
:return: None
'''
# 构造列标签名字,注:如果数据中没有标签,则pandas会默认把第一行数据作为特征名,这里数据是没有标签名的,所以这里手动添加
column = ['Sample code number','Clump Thickness','Uniformity of Cell Size','Uniformity of Cell Shape','Marginal Adhesion','Single Epithelial Cell Size'
,'Bare Nuclei','Bland Chromatin','Normal Nucleoli','Mitoses','Class']
data = pd.read_csv("http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/breast-cancer-wisconsin.data"
, names=column)

#缺失值进行处理
data = data.replace(to_replace='?',value=np.nan)
data = data.dropna()

# 数据分割
x_train,x_test,y_train,y_test = train_test_split(data[column[1:10]],data[column[10]],test_size=0.25)

#进行标准化
std = StandardScaler()
x_train = std.fit_transform(x_train)
x_test = std.transform(x_test)

#逻辑回归预测
lg = LogisticRegression(penalty='l2',C=1.0)
lg.fit(x_train,y_train)
y_predict = lg.predict(x_test)
print("计算的回归系数为:",lg.coef_)
'''
计算的回归系数为: [[1.07139763 0.920439 1.03352718 0.88427896 0.07783286 1.15006497
0.94210002 0.59530658 0.81971895]]
'''
print("准确率为:",lg.score(x_test,y_test))
'''
准确率为: 0.9590643274853801
'''
print("召回率:",classification_report(y_test,y_predict,labels=[2,4],target_names=["良性","恶性"]))
'''
召回率: precision recall f1-score support

良性 0.96 0.97 0.97 110
恶性 0.95 0.93 0.94 61

micro avg 0.96 0.96 0.96 171
macro avg 0.96 0.95 0.96 171
weighted avg 0.96 0.96 0.96 171
'''
return None


if __name__ == "__main__":
logistic()

非监督学习:K-均值聚类(k-means)算法

K-均值聚类属于无监督学习。那么监督学习和无监督学习的区别在哪儿呢?监督学习知道从对象(数据)中学习什么,而无监督学习无需知道所要搜寻的目标,它是根据算法得到数据的共同特征。比如用分类和聚类来说,分类事先就知道所要得到的类别,而聚类则不一样,只是以相似度为基础,将对象分得不同的簇。

K-Means算法有大量的变体,包括初始化优化K-Means++, 距离计算优化elkan K-Means算法和大数据情况下的优化Mini Batch K-Means算法。

算法流程

K-means是一个反复迭代的过程,算法分为四个步骤:

  1. 选取数据空间中的K个对象作为初始中心,每个对象代表一个聚类中心;
    • 我们会根据对数据的先验经验选择一个合适的k值,如果没有什么先验知识,则可以通过交叉验证选择一个合适的k值。
  2. 对于样本中的数据对象,根据它们与这些聚类中心的欧氏距离,按距离最近的准则将它们分到距离它们最近的聚类中心(最相似)所对应的类;
  3. 更新聚类中心:将每个类别中所有对象所对应的均值作为该类别的聚类中心,计算目标函数的值;
  4. 判断聚类中心和目标函数的值是否发生改变,若不变,则输出结果,若改变,则返回2)

这里发现了一个python2.7版本的简易实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from numpy import *
from math import sqrt

def loadDataSet(fileName): #general function to parse tab -delimited floats
dataMat = [] #assume last column is target value
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = map(float,curLine) #map all elements to float()
dataMat.append(fltLine)
return dataMat
\#计算欧氏距离
def distEclud(vecA, vecB):
return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB)

def randCent(dataSet, k):
n = shape(dataSet)[1]
centroids = mat(zeros((k,n)))#create centroid mat
for j in range(n):#create random cluster centers, within bounds of each dimension
minJ = min(dataSet[:,j])
rangeJ = float(max(dataSet[:,j]) - minJ)
centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))
return centroids

\#dataSet样本点,k 簇的个数
\#disMeas距离量度,默认为欧几里得距离
\#createCent,初始点的选取
def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
m = shape(dataSet)[0] #样本数
clusterAssment = mat(zeros((m,2))) #m*2的矩阵
centroids = createCent(dataSet, k) #初始化k个中心
clusterChanged = True
while clusterChanged: #当聚类不再变化
clusterChanged = False
for i in range(m):
minDist = inf; minIndex = -1
for j in range(k): #找到最近的质心
distJI = distMeas(centroids[j,:],dataSet[i,:])
if distJI < minDist:
minDist = distJI; minIndex = j
if clusterAssment[i,0] != minIndex: clusterChanged = True
# 第1列为所属质心,第2列为距离
clusterAssment[i,:] = minIndex,minDist**2
print centroids

# 更改质心位置
for cent in range(k):
ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]
centroids[cent,:] = mean(ptsInClust, axis=0)
return centroids, clusterAssment

伪代码如下:
function K-Means(输入数据,中心点个数K)
获取输入数据的维度Dim和个数N
随机生成K个Dim维的点
while(算法未收敛)
对N个点:计算每个点属于哪一类。
对于K个中心点:
1,找出所有属于自己这一类的所有数据点
2,把自己的坐标修改为这些数据点的中心点坐标
end
输出结果:
end

sklearn.cluster.KMeans(n_clusters=8,init=’k-means++’)

  • n_clusters:开始的聚类中心数量
  • init:初始化方法,默认为’k-means++’
  • labels_:默认标记的类型,可以和真实值比较(不是值比较)

python与机器学习入门一

为什么要写本文

机器学习涉及到很多数学相关基础知识,概率论,高数算法等,而直接去看这些课本或书籍,简直就是晦涩难懂,哪怕是西瓜书这种通俗易懂的书籍,对于数学不好的人来讲也有点像天书。为了让小白可以较轻松的掌握一些机器学习相关知识,还是要从案例出发,通过案例来学习用到的数学知识,熟悉各种算法的原理,顺便将用到的库和框架进行简单介绍,而且可以结合场景解决实际问题,提高初学者幸福感。
不过在此还是要列举一些书籍可供参考:

  • 概率论和高数,直接大学课本就好
  • 《机器学习》:俗称西瓜书,学习机器学习的经典书籍
  • 《Python数据分析与挖掘实战》:从数据挖掘的应用出发,以电力、航空、医疗、互联网、生产制造以及公共服务等行业真实案例为主线,深入浅出介绍Python数据挖掘建模过程,实践性极强。
  • 《TensorFlow技术解析与实战》:包揽TensorFlow1.1的新特性 人脸识别 语音识别 图像和语音相结合等热点一应俱全

机器学习是什么

机器学习是人工智能的一个分支。机器学习算法是一类从数据中自动分析获得规律,并利用规律对未知数据进行预测的算法。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能。 它是人工智能的核心,是使计算机具有智能的根本途径,其应用遍及人工智能的各个领域,它主要使用归纳、综合而不是演绎。因为学习算法中涉及了大量的统计学理论,机器学习与推断统计学联系尤为密切,也被称为统计学习理论。

机器学习应用在哪些方面?

维基百科:机器学习已广泛应用于数据挖掘、计算机视觉、自然语言处理、生物特征识别、搜索引擎、医学诊断、检测信用卡欺诈、证券市场分析、DNA序列测序、语音和手写识别、战略游戏和机器人等领域。
新闻等应用上的推荐类功能,人脸识别相关功能:直播时的美颜、加装饰等等;
这里有一个集合,最具价值的50个机器学习应用[2017年]:https://bigquant.com/community/t/topic/6909

数据

python深度学习中,读取数据一般是通过pandas读取文件,之所以不读取数据库,是因为数据库读取速度会成为性能瓶颈。

可用数据集:

  • Kaggle:1、大数据竞赛平台,真实数据,数据量巨大
  • UCI:1,覆盖科学、生活、经济等领域
  • scikit-learn:数据量小,方便学习

常用数据集结构:特征值+目标值
注:有些数据可以没有目标值
数据中对于特征的处理:

  • pandas:一个数据读取非常方便以及基本的处理格式的工具
  • 对于特征的处理提供了强大的接口

特征工程

什么是特征工程(目的)

一句话:特征工程是将原始数据转换为更好的代表预测模型的潜在问题的特征的过程,从而提高了对未知数据的预测准确性。
“数据决定了机器学习的上限,而算法只是尽可能逼近这个上限”,这里的数据指的就是经过特征工程得到的数据。特征工程指的是把原始数据转变为模型的训练数据的过程,它的目的就是获取更好的训练数据特征,使得机器学习模型逼近这个上限。特征工程能使得模型的性能得到提升,有时甚至在简单的模型上也能取得不错的效果。特征工程在机器学习中占有非常重要的作用,一般认为括特征构建、特征提取、特征选择三个部分。特征构建比较麻烦,需要一定的经验。 特征提取与特征选择都是为了从原始特征中找出最有效的特征。它们之间的区别是特征提取强调通过特征转换的方式得到一组具有明显物理或统计意义的特征;而特征选择是从特征集合中挑选一组具有明显物理或统计意义的特征子集。两者都能帮助减少特征的维度、数据冗余,特征提取有时能发现更有意义的特征属性,特征选择的过程经常能表示出每个特征的重要性对于模型构建的重要性。
特征工程的目的是筛选出更好的特征,获取更好的训练数据.

特征工程流程:

    1. 数据采集 / 清洗 / 采样
      • 1.1:数据采集:数据采集前需要明确采集哪些数据,一般的思路为:哪些数据对最后的结果预测有帮助?数据我们能够采集到吗?线上实时计算的时候获取是否快捷?
      • 1.2:数据清洗: 数据清洗也是很重要的一步,机器学习算法大多数时候就是一个加工机器,至于最后的产品如何,取决于原材料的好坏。数据清洗就是要去除脏数据,比如某些商品的刷单数据。
      • 1.3:数据采样:采集、清洗过数据以后,正负样本是不均衡的,要进行数据采样。采样的方法有随机采样和分层抽样。但是随机采样会有隐患,因为可能某次随机采样得到的数据很不均匀,更多的是根据特征采用分层抽样。  
  • 2.特征处理
    • 2.1:数值型
      • 幅度调整/归一化:python中会有一些函数比如preprocessing.MinMaxScaler()将幅度调整到 [0,1] 区间。
      • 统计值:包括max, min, mean, std等。python中用pandas库序列化数据后,可以得到数据的统计值。
    • 2.2:类别型,类别型一般是文本信息,比如颜色是红色、黄色还是蓝色,我们存储数据的时候就需要先处理数据
      • one-hot编码,编码后得到哑变量。统计这个特征上有多少类,就设置几维的向量,pd.get_dummies()可以进行one-hot编码。
      • Hash编码成词向量
      • Histogram映射:把每一列的特征拿出来,根据target内容做统计,把target中的每个内容对应的百分比填到对应的向量的位置。优点是把两个特征联系起来。
    • 2.3:时间型
      • 连续值
      • 离散值
    • 2.4:文本型
      • 词袋:文本数据预处理后,去掉停用词,剩下的词组成的list,在词库中的映射稀疏向量。Python中用CountVectorizer处理词袋.
      • 把词袋中的词扩充到n-gram:n-gram代表n个词的组合。
    • 2.5:统计型
      • 加减平均,商品价格高于平均价格多少,用户在某个品类下消费超过平均用户多少,用户连续登录天数超过平均多少…
      • 分位线,商品属于售出商品价格的多少分位线处
      • 次序型
      • 比例类
    • 2.6:组合特征
      • 拼接型
      • 模型特征组合
  • 3.特征选择:特征选择,就是从多个特征中,挑选出一些对结果预测最有用的特征。特征选择和降维有什么区别呢?前者只踢掉原本特征里和结果预测关系不大的, 后者做特征的计算组合构成新特征。
    3.1:过滤型,评估单个特征和结果值之间的相关程度, 排序留下Top相关的特征部分。
    3.2:包裹型,把特征选择看做一个特征子集搜索问题, 筛选各种特征子集, 用模型评估效果。 典型算法:“递归特征删除算法”。
    3.3:嵌入型,根据模型来分析特征的重要性,最常见的方式为用正则化方式来做特征选择。

scikit-learn工具

  • Python语言的机器学习工具基于Numpy和Scipy
  • 提供了大量用于数据挖掘和分析的工具,包括数据预处理、交叉验证、算法与可视化算法等一系列接口。
  • 文档完善,容易上手

特征抽取

API:sklearn.feature_extraction

字典特征数据抽取:把字典中一些类别数据,转换为特征值,如One-hot编码形势。

  • DictVectorizer

文本特征抽取:对文本数据进行特征值化
文本抽取默认不会对中文进行分词,而是按照空格逗号等进行分。所以在fit_transform之前要进行分词。

  • CountVectorizer:简单的频率统计
  • TfidfVectorizer:tf-idf算法
    • 在将文本分词并向量化后,我们可以得到词汇表中每个词在各个文本中形成的词向量,但是这时候的词向量里,有一些类似”to”,”my”,”is”的词,出现的频率可能会高于文章的中心词汇,如果我们的向量化特征仅仅用词频表示就无法反应这一点。因此我们需要进一步的预处理来反应文本的这个特征,而这个预处理就是TF-IDF。
    • TF-IDF是Term Frequency - Inverse Document Frequency的缩写,即“词频-逆文本频率”。它由两部分组成,TF和IDF。
      • TF也就是词频,我们做的向量化也就是做了文本中各个词的出现频率统计。

        词频(TF) = 某个词在文章中的出现次数
        词频(TF) = 某个词在文章中的出现次数 / 文章总词数。这里是因为文章有长短之分,为了便于不同文章的比较,做”词频”标准化.

      • IDF,即“逆文本频率”,IDF反应了一个词在所有文本中出现的频率,如果一个词在很多的文本中出现,那么它的IDF值应该低,比如上文中的“to”。而反过来如果一个词在比较少的文本中出现,那么它的IDF值应该高。比如一些专业的名词

        逆文档频率(IDF) = log(语料库的文档总数/包含该词的文档总数+1)

    • TF-IDF = 词频(TF) * 逆文档频率(IDF),TF-IDF与一个词在文档中的出现次数成正比,与该词在整个语言中的出现次数成反比。

特征抽取简单demo及API如下:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from sklearn.feature_extraction import DictVectorizer
from sklearn.feature_extraction.text import CountVectorizer

def dictvec():
'''
字典数据抽取
:return: None
'''
# 实例化
dict = DictVectorizer(sparse=False) # sparse指定fit_transform的结果是否转换为sparse矩阵,False的话则不是sparse矩阵而是数组形势
# 调用fit_transform
data = dict.fit_transform(
[{'city': '北京', 'temperature': 100}, {'city': '上海', 'temperature': 60}, {'city': '深圳', 'temperature': 30}]) #参数是列表
print(data)
'''
sparse = False情况下,这也叫 One-hot编码:
[[ 0. 1. 0. 100.]
[ 1. 0. 0. 60.]
[ 0. 0. 1. 30.]]
sparse默认为true情况下:
(0, 1) 1.0
(0, 3) 100.0
(1, 0) 1.0
(1, 3) 60.0
(2, 2) 1.0
(2, 3) 30.0
'''
print(dict.get_feature_names()) # 返回类别名称,如这里:['city=上海','city=北京','city=深圳','temperature']
return None

def countvec():
'''
对文本进行特征值化
:return: None
'''
cv = CountVectorizer() #这里没有sparse参数,后续如果要转换为数组,则要用fit_transform产生的sparse矩阵调用toarray()函数
data = cv.fit_transform(["To the world \\u9752\\u9752 may be just one person"," To me \\u9752\\u9752 may be the world"]) # 参数是列表

print(cv.get_feature_names())
'''
统计所有词,单个的英文字母不会统计,比如‘i’,结果为:
['be', 'just', 'may', 'me', 'one', 'person', 'the', 'to', 'u9752', 'world']
'''
print(data.toarray())
'''
对应词出现次数
[[1 1 1 0 1 1 1 1 2 1]
[1 0 1 1 0 0 1 1 2 1]]
'''


return None


if __name__ == "__main__":
# dictvec()
countvec()

特征预处理

通过特定的统计方法讲数据转换为算法要求的数据。

sklearn.preprocessing

  • 归一化,防止某个数据对结果影响太大 sklearn.preprocessing.MinMaxScaler
    • 通过将原始数据进行变换,把原始数据映射到[0,1]之间
    • 原理:作用于每一列,max为一列的最大值,min为一列的最小值
    • 缺点:异常点对此影响较大,这种情况下要使用标准化来解决
  • 标准化,异常值影响较小 sklearn.preprocessing.StandardScaler
    • 通过对原始数据进行变换,把数据变换到均值为0,标准差为1的范围内
    • 在已有样本足够多的情况下比较稳定,适合现代嘈杂的大数据场景
    • 原理:

归一化与标准化示例如下:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from sklearn.preprocessing import MinMaxScaler,StandardScaler

def mm():
'''
归一化处理
:return: None
'''
mm = MinMaxScaler(feature_range=(0,1)) #参数可以指定归一化后的范围,默认为[0,1]
data = mm.fit_transform([[10,2,5,40],[20,4,5,8],[30,6,5,5]])
print(data)
'''
归一化是按照列来归一的
[[0. 0. 0. 1. ]
[0.5 0.5 0. 0.08571429]
[1. 1. 0. 0. ]]
'''
return None

def stand():
'''
标准化缩放
:return: None
'''
std = StandardScaler()
data = std.fit_transform([[1,-1,3],[2,4,2],[4,6,-1]])
print(data)
'''
转换后的值,每一列均值为0,标准差为1
[[-1.06904497 -1.35873244 0.98058068]
[-0.26726124 0.33968311 0.39223227]
[ 1.33630621 1.01904933 -1.37281295]]
'''
return None

if __name__ == "__main__":
#mm()
stand()

数据降维

特征选择

特征选择原因:如果训练数据包含许多冗余无关特征,因而移除这些特征并不会导致丢失信息

  • 数据冗余:部分特征相关度高,容易消耗计算机性能
  • 缩短训练时间
  • 改善通用性、降低过拟合

特征选择是什么?

特征选择就是单纯的从提取到的所有特征中选择部分特征作为训练集特征,特征在选择前和选择后可以改变值、也可以不改变值,但是选择后的特征维数肯定比选择前小,因为只是选择了其中一部分特征。

主要方法:

  • Filter(过滤式):
    • VarianceThreshold(方差过滤):由于方差为0的时候,一列的数据是一样的,所以是对结果没用的数据,由此推论,当方差小于某个值的时候,这个特征是不具有参考性的,可以剔除。
      • sklearn.feature_selection.VarianceThreshold
  • Embedded(嵌入式):正则化、决策树
  • Wrapper(包裹式)
  • 神经网络

主成分分析(PCA,principal Component Analysis)

背景:

在许多领域的研究与应用中,通常需要对含有多个变量的数据进行观测,收集大量数据后进行分析寻找规律。多变量大数据集无疑会为研究和应用提供丰富的信息,但是也在一定程度上增加了数据采集的工作量。更重要的是在很多情形下,许多变量之间可能存在相关性,从而增加了问题分析的复杂性。如果分别对每个指标进行分析,分析往往是孤立的,不能完全利用数据中的信息,因此盲目减少特征会损失很多有用的信息,从而产生错误的结论。因此需要找到一种合理的方法,在减少需要分析的特征同时,尽量减少信息的损失,以达到对所收集数据进行全面分析的目的。由于各变量之间存在一定的相关关系,因此可以考虑将关系紧密的变量变成尽可能少的新变量,使这些新变量是两两不相关的,那么就可以用较少的综合指标分别代表存在于各个变量中的各类信息。主成分分析与因子分析就属于这类降维算法。
说白了就是特征太多情况下,如果很多特征之间是有联系的,这时直接通过特征选择等方式来剔除掉某些特征,会导致失去一些有用信息。所以需要一种方法,在减少特征数量的同时,尽可能少的减少信息的损失。

PCA是什么?

是一种使用最广泛的数据压缩算法。在PCA中,数据从原来的坐标系转换到新的坐标系,由数据本身决定。转换坐标系时,以方差最大的方向作为坐标轴方向,因为数据的最大方差给出了数据的最重要的信息。第一个新坐标轴选择的是原始数据中方差最大的方法,第二个新坐标轴选择的是与第一个新坐标轴正交且方差次大的方向。重复该过程,重复次数为原始数据的特征维数。
通过这种方式获得的新的坐标系,我们发现,大部分方差都包含在前面几个坐标轴中,后面的坐标轴所含的方差几乎为0,于是,我们可以忽略余下的坐标轴,只保留前面的方差较大的坐标轴。事实上,这样也就相当于只保留包含绝大部分方差的维度特征,而忽略包含方差几乎为0的特征维度,也就实现了对数据特征的降维处理。
PCA里,特征数量会减少,但数据也会改变。一般特征数量达到上百才会用

作用:

可以削减回归分析或者聚类分析中特征的数量

原理:
一张结果图:

详细过程参考这里的‘3.5PCA算法两种实现方法’:https://blog.csdn.net/program_developer/article/details/80632779

数据降维的两个方法demo:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from sklearn.feature_selection import VarianceThreshold
from sklearn.decomposition import PCA
def var():
'''
特征选择-删除低方差的特征
:return: None
'''
var = VarianceThreshold(threshold=0.0) # threshold指定方差阈值
data = var.fit_transform([[0,2,0,3],[0,1,4,3],[0,1,1,3]]) #数据的第一列和最后一列方差为0
print(data)
'''
过滤掉了第一列和最后一列
[[2 0]
[1 4]
[1 1]]
'''
return None

def pca():
'''
主成分分析进行特征降维
:return: None
'''
pca = PCA(n_components=0.9) # n_components可以选择保留的数据信息比例,一般为90%~95%
data = pca.fit_transform([[2,4,6,8],[5,4,8,6],[1,5,6,3]])
print(data)
'''
[[-1.32007718 2.16837282]
[-1.95237119 -1.90596201]
[ 3.27244837 -0.26241081]]
'''
return None

if __name__ == "__main__":
pca()

机器学习基础

算法是核心,数据和计算是基础。

机器学习算法判别依据:数据类型

  • 离散型数据:由记录不同类别个体的数目所得到的数据,又称计数数据,所有这些数据全部都是整数,而且不能再细分,也不能进一步提高他们的精确度。区间内不可分
  • 连续型数据:变量可以在某个范围内取任一数,即变量的取值可以是连续的,如长度、时间、质量等,这类数值通常含有小数部分。区间内可分

机器学习算法分类:

  • 监督学习(预测):有特征值和目标值,有标准答案
    • 分类:对应数据类型为离散型
      • k-近邻算法
      • 贝叶斯分类
      • 决策树与随机森林
      • 逻辑回归
      • 神经网络
    • 回归:对应数据类型为连续型
      • 线性回归
      • 岭回归
    • 标注
      • 隐马尔科夫模型
  • 无监督学习:只有特征值
    • 聚类
      • k-means

机器学习开发流程

  • 获取数据

    包括获取原始数据以及从原始数据中经过特征工程从原始数据中提取训练、测试数据

  • 特征工程

    包括从原始数据中特征构建、特征提取、特征选择。特征工程做的好能发挥原始数据的最大效力,往往能够使得算法的效果和性能得到显著的提升,有时能使简单的模型的效果比复杂的模型效果好。数据挖掘的大部分时间就花在特征工程上面,是机器学习非常基础而又必备的步骤。数据预处理、数据清洗、筛选显著特征、摒弃非显著特征等等都非常重要。

  • 训练模型、诊断、调优

    诊断中至关重要的是判断过拟合、欠拟合,常见的方法是绘制学习曲线,交叉验证。通过增加训练的数据量、降低模型复杂度来降低过拟合的风险,提高特征的数量和质量、增加模型复杂来防止欠拟合。诊断后的模型需要进行进一步调优,调优后的新模型需要重新诊断

  • 模型验证、误差分析

    主要是分析出误差来源与数据、特征、算法。

  • 模型融合

    提升算法的准确度主要方法是模型的前端(特征工程、清洗、预处理、采样)和后端的模型融合

  • 上线运行

    通过提供API等形式来上线

sklearn数据集

1.数据集划分

机器学习一般的数据集会划分为两个部分:

  • 训练数据:用户训练、构建模型
  • 测试数据:在模型检验时使用,用于评估模型是否有效

2.sklearn数据集API介绍

  • sklearn.model_selection.train_test_split
  • sklearn.datasets
    • datasets.load_*(); 获取小规模数据集,数据包含在datasets里
    • datasets.fetch_*(data_home=None); 获取大规模数据集,需要从网络上下载,第一个参数是data_home表示数据集下载的目录,默认 ‘~/scikit_learn_data/‘
    • load*和fetch*返回的数据类型都是datasets.base.Bunch(字典格式)
      • data:特征数据数组,是[n_samples * m_features]的二维numpy.ndarry数组
      • target:标签数组,是n_samples的一维numpy.ndarray数组
      • DESCR:数据描述
      • feature_names:特征名,新闻数据、手写数字、回归数据集没有
      • target_names:标签名

3.sklearn分类数据集,对应分类算法

此处以内置数据集 load_iris和20newsgroups 为例

1.load_iris()获取内置普通分类用的数据集
2.获取用于分类测试的大数据集,sklearn.datasets.fetch_20newgroups(),数据集收集了大约20,000左右的新闻组文档,均匀分为20个不同主题的新闻组集合。

datasets.clear

数据集进行分割:
sklearn.model_selection.train_test_split(arrays,*options)

  • x:数据集的特征值
  • y:数据集的标签值
  • test_size:测试集大小,一般为float
  • random_state:随机数种子
  • return:训练集特征值,测试集特征值,训练标签,测试标签

下面为两个数据集的获取代码,目前仅为数据采集,至于特征提取、模型训练等后续再写

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

from sklearn.datasets import load_iris, fetch_20newsgroups
from sklearn.model_selection import train_test_split

'''
内置的数据集,加载并返回鸢尾花数据集
类别:3
特征:4
样本数量:150
每个类别数量:50
'''

li = load_iris()

print(li.data) # 获取特征数据数组
'''
[[5.1 3.5 1.4 0.2]
[4.9 3. 1.4 0.2]
[4.7 3.2 1.3 0.2]
......
[5.9 3. 5.1 1.8]]
'''

print(li.target) # 获取目标值
'''
目标值是离散型,0,1,2
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2]
.. _iris_dataset:
'''
# print(li.DESCR) #打印出一些描述信息

train_feature, test_feature, train_target, test_target = train_test_split(li.data, li.target,
test_size=0.25) # 参数:特征值,目标值,测试集大小
# 返回值依次为:训练集的特征值、测试集特征值、训练集目标值、测试集目标值

print("训练集特征值和目标值:", train_feature, train_target)
'''
训练集特征值和目标值: [[6.4 3.2 5.3 2.3]
[4.6 3.6 1. 0.2]
[5.5 2.5 4. 1.3]
......
[5.7 2.8 4.1 1.3]
[5.5 2.4 3.7 1. ]]
[2 0 1 1 2 2 0 1 1 0 2 2 2 0 1 1 2 2 1 1 1 0 0 2 1 2 1 0 1 1 0 0 1 0 0 0 2
2 0 0 1 1 1 1 1 0 1 2 0 0 0 2 1 0 0 1 1 0 1 1 1 0 0 2 0 1 2 2 2 0 2 0 2 2
0 1 2 0 1 0 2 2 2 0 2 0 1 0 1 0 1 0 2 1 1 1 0 1 1 1 2 2 2 0 0 2 2 2 0 2 1
1]
'''

print("测试集特征值和目标值:", test_feature, test_target)
'''
测试集特征值和目标值: [[6.3 2.9 5.6 1.8]
[6.3 2.5 4.9 1.5]
[7.3 2.9 6.3 1.8]
......
[4.4 2.9 1.4 0.2]
[5.1 3.4 1.5 0.2]]
[2 1 2 2 2 1 2 0 2 2 2 0 0 2 0 0 1 1 1 1 2 2 0 2 2 1 1 2 2 0 0 1 1 0 0 2 0 0]
'''

'''
也是一个内置的数据集,不过是是大数据集,会从网络下载
'''
news = fetch_20newsgroups(data_home=r"G:\pyMechineLearn", subset='all')
print(news.data)
'''
貌似是一个list类型,每一个元素是str类型,也就是一篇文章。
'''
print(news.target)
'''
不同的数对应不同的文章类型
[10 3 17 ... 3 1 7]
'''

4.sklearn回归数据集,对应回归算法

此处以内置数据集 波士顿房价数据集为例
sklearn.datasets.load_boston()

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

from sklearn.datasets import load_boston

'''
波士顿房价数据
目标类别:5-50
特征:13
样本数量:506
'''

lb = load_boston()

print(lb.data) #获取特征值
'''
特征值就是收集的属性,房子的一些大小等信息
[[6.3200e-03 1.8000e+01 2.3100e+00 ... 1.5300e+01 3.9690e+02 4.9800e+00]
[2.7310e-02 0.0000e+00 7.0700e+00 ... 1.7800e+01 3.9690e+02 9.1400e+00]
[2.7290e-02 0.0000e+00 7.0700e+00 ... 1.7800e+01 3.9283e+02 4.0300e+00]
...
[6.0760e-02 0.0000e+00 1.1930e+01 ... 2.1000e+01 3.9690e+02 5.6400e+00]
[1.0959e-01 0.0000e+00 1.1930e+01 ... 2.1000e+01 3.9345e+02 6.4800e+00]
[4.7410e-02 0.0000e+00 1.1930e+01 ... 2.1000e+01 3.9690e+02 7.8800e+00]]
'''
print(lb.target) #获取目标值
'''
[24. 21.6 34.7 33.4 36.2 28.7 22.9 27.1 16.5 18.9 15. 18.9 21.7 20.4
18.2 19.9 23.1 17.5 20.2 18.2 13.6 19.6 15.2 14.5 15.6 13.9 16.6 14.8
.......
8.1 13.6 20.1 21.8 24.5 23.1 19.7 18.3 21.2 17.5 16.8 22.4 20.6 23.9
22. 11.9]
'''
print(lb.DESCR) #描述信息
'''
\*\*Data Set Characteristics:\*\*
:Number of Instances: 506
:Attribute Information (in order):
- CRIM per capita crime rate by town
- ZN proportion of residential land zoned for lots over 25,000 sq.ft.
- INDUS proportion of non-retail business acres per town
.........
- MEDV Median value of owner-occupied homes in $1000's
'''

转换器(Transformer)与估计器(estimator)

特征工程中,实例化出来的可调用fit_transform API的对象(如 DictVectorizer等)就是一个Transformer

  • fit_transform():输入数据直接转换
  • fit()+transform():fit是单纯的输入数据,然后进行一些预先处理,如计算平均值,方差等。transform是进行数据的转换

在sklearn中,估计器(estimator)是一个重要的角色,是一类实现了算法的API,这玩意不是知道接口是啥就可以,而是传入的算法参数- -。。

  • 用于分类的估计器:
    • sklearn.neighbors k-近邻算法
    • sklearn.naive_bayes 贝叶斯
    • sklearn.linear_model.logisticRegression 逻辑回归
    • sklearn.tree 决策树与随机森林
  • 用于分类的估计器:
    • sklearn.linear_model.linearRegression 线性回归
    • sklearn.linear_model.Ridge 岭回归

python数据分析基础

前言

  • Python VS R 语言(原文:https://mp.weixin.qq.com/s/CAsVeEJlgru85NUHcUKv6A):
    • 什么是R语言?
      R语言,一种自由软件编程语言与操作环境,主要用于统计分析、绘图、数据挖掘。R基于S语言的一个GNU计划项目,所以也可以当作S语言的一种实现。
    • Python与R语言共同特点
    • Python和R在数据分析和数据挖掘方面都有比较专业和全面的模块,很多常用的功能,比如矩阵运算、向量运算等都有比较高级的用法
    • Python和R两门语言有多平台适应性,linux、window都可以使用,并且代码可移植性强
    • Python和R比较贴近MATLAB以及minitab等常用的数学工具
    • Python与R语言区别
    • 数据结构方面,由于是从科学计算的角度出发,R中的数据结构非常的简单,主要包括向量(一维)、多维数组(二维时为矩阵)、列表(非结构化数据)、数据框(结构化数据)。而 Python 则包含更丰富的数据结构来实现数据更精准的访问和内存控制,多维数组(可读写、有序)、元组(只读、有序)、集合(唯一、无序)、字典(Key-Value)等等。
    • Python与R相比速度要快。Python可以直接处理上G的数据;R不行,R分析数据时需要先通过数据库把大数据转化为小数据(通过groupby)才能交给R做分析,因此R不可能直接分析行为详单,只能分析统计结果。
    • Python是一套比较平衡的语言,各方面都可以,无论是对其他语言的调用,和数据源的连接、读取,对系统的操作,还是正则表达和文字处理,Python都有着明显优势。 而R是在统计方面比较突出。
    • 总结
      总的来说,Python 的 pandas 借鉴了R的dataframes,R 中的 rvest 则参考了 Python的BeautifulSoup,两种语言在一定程度上存在互补性,通常,我们认为 Python 比 R 在计算机编程、网络爬虫上更有优势,而 R 在统计分析上是一种更高效的独立数据分析工具。所以说,同时学会Python和R这两把刷子才是数据科学的王道。

python其实是可以直接调用R语言的函数的,只需要安装rpy2模块即可。

matplotlib

提到数据分析,就不得不考虑到数据分析后的展示,Matplotlib是一个Python 2D绘图库,它可以在各种平台上以各种硬拷贝格式和交互式环境生成出具有出版品质的图形。
至于matplotlib的安装,可以通过 Anaconda来安装,相关操作不多赘述。

折线图示例

相关api在代码中都有注释
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# coding=utf-8
# 折线图
from matplotlib import pyplot as plt
import matplotlib

# 注:matplotlib默认时不支持中文的,可通过rc修改
font = {'family': 'SimHei',
'weight': 'bold',
'size': '16'}
matplotlib.rc('font', **font)

x = range(2, 26, 2)
y = [15, 13, 12, 11, 15, 3, 20, 22, 14.5, 6.3, 21.2, 10]
# 要在一个图形上绘制多个曲线,可直接设置另一个列表然后通过plot来绘制
another_y = [10, 10, 22, 15, 13, 5, 6, 8, 11.5, 20, 21.2, 20]

# 设置图片大小
# plt.figure(figsize=(20, 8), dpi=80)

# 绘图,label为折线名字,需要在后面添加legend方法才能显示在图形上,还可设置线条颜色和style等
plt.plot(x, y, label="第一条")
# 绘制另一条曲线
plt.plot(x, another_y, label="第二条")

# 绘制x轴的刻度,根据列表参数的值展示刻度
# plt.xticks(x)
# plt.xticks(range(2,25))
# x轴刻度显示字符串, 需要构造一个和xticks第一个参数一一对应的字符串列表,这也可以解决xy轴刻度对不准问题,自定义在哪里显示刻度和显示什么
_xstring_labels = ["string{}".format(i) for i in x]
plt.xticks(x, _xstring_labels, rotation=90) # rotation可以让x刻度旋转一定角度

# 展示y轴刻度
plt.yticks(range(min(y), max(y)))

# 添加图形和坐标轴的描述信息
plt.xlabel("x轴信息")
plt.ylabel("y轴信息")
plt.title("图形名字")

# 绘制网格,参数alpha可以设置透明度
plt.grid()

# 添加图例,loc参数可设置图例位置
plt.legend()

# 展示图形
plt.show()

# 保存图形,后缀为svg可以保存为矢量图
# plt.savefig("./testone.png")

其他图形示例

绘制其他图形的api和折线图几乎相同,程序参考上面就可以,下面列举一下api以供参考
其中 x,y为x和y轴的列表

  • 散点图
    • plt.scatter(x,y)
  • 条形图
    • plt.bar(x,y,width=’0.3’) #width为设置线条的粗细
    • plt.hbar(x,y,height=’0.3’) #绘制横向的条形图,height为横向线条的粗细
    • 通过设置 xticks可以实现数字和字符串的相对应
  • 直方图
    • plt.hist(a,num_bins,normed = True) #a为数据列表,num_bins为数据分为多少组,normed用来设置是否是频率直方图。
      • 组数 = 极差/组距 = (max(a) - min(a))/自己设置的组距,注:最大值-最小值的结果一定要能被组距整除,不然图像会出现偏差
    • 当组距不均匀时,plt.hist(a,[min(a)+i*组距 for i in range(组数)]),也就是可以传入一个列表,长度为组数,值为分组的依据
    • 小技巧,设置x轴刻度:plt.xticks(range(min(a),man(a)+d,d)) ,因为range是去[),所以最后不加d的话会导致最后一个刻度丢失
  • more

其他的一些绘制工具

Numpy

什么是Numpy

NumPy系统是Python的一种开源的数值计算扩展,是python科学计算的基础包。这种工具可用来存储和处理大型矩阵,具有快速高效的多维数组对象ndarray,比Python自身的嵌套列表(nested list structure)结构要高效的多。NumPy(Numeric Python)提供了许多高级的数值编程工具,如:矩阵数据类型、矢量处理,以及精密的运算库。专为进行严格的数字处理而产生。可以用来处理线性代数运算、傅里叶变换以及随机数生成等。

下面列出了一些列numpy常用的API可供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# coding = utf-8
import numpy
import random

# numpy 创建数组
t1 = numpy.array([1, 2, 3]) # [1,2,3]
t2 = numpy.array(range(1, 6)) # [1,2,3,4,5]
t3 = numpy.arange(4, 10, 2) # [4,6,8]

print(t3.dtype) # int32 与平台有关,还可能是int64,float32等
t4 = numpy.arange(4, 10, 2, dtype="int64") # 可以手动指定数值类型
# 调整数据类型
t5 = t4.astype("float32")
print(t5.dtype)

t6 = random.random()
t6 = numpy.round(t6, 2) # round保留两位小数
print(t6)

'''
numpy读取本地CSV文件
api:numpy.loadtxt(fname, dtype=, comments='#', delimiter=None, converters=None, skiprows=0, usecols=None, unpack=False, ndmin=0)
fname :文件或字符串
dtype : 数据类型
comments :用于指示注释开头的字符,默认值为 : '#'.
delimiter :用于分隔值的字符,缺省值为任何空白字符,如空格 ,制表符
converters : dict字典 ,用来定义将对应的列转换为浮点数的函数。列入:0列是日期字符串:"converters={0:datestr2num}".convertes 同时也能够为丢失的数据设置缺省值:"convertes={3:lambda s:float(s.strip() or 0)}"
skiprows:跳过开头的行数
usecols : 确定那几列被读取
unpack :是否进行转置
'''
# n1 = numpy.loadtxt("test.csv",delimiter=",",dtype="int") # t1= [ [0行0列 0行1列 0行2列] [1行0列 1行1列 1行2列] [2行0列 2行1列 2行2列] ]

n2 = numpy.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
])
# 索引和切片
# print(n2[行,列]) ‘:’表所有
# 取一行
print(n2[1]) # [5 6 7 8]
# 取连续多行
print(n2[1:]) # [[5 6 7 8] [9 10 11 12]]
# 取不连续多行
print(n2[[0, 2]]) # [ [1 2 3 4] [9 10 11 12]]

# 取列
print(n2[:, 0]) # [1 5 9]
# 连续多列
print(n2[:, 2:]) # [ [3 4] [7 8 ] [11 12]]
# 不连续的列
print(n2[:, [0, 2]]) # [ [1 3][5 7][9 11] ]

# 取单行单列和多行多列
print(n2[[0, 1], [1, 2]]) # 注意,这里是 [2 7]
print(n2[1, 2]) # [7]
print(n2[0:2, 0:3]) # [[1 2 3][5 6 7]] ,这里的多行多列,‘:’的区间是[)

# 数据拼接
n3 = numpy.array([
[1, 2, 3, 4],
[5, 6, 7, 8]
])
n4 = numpy.array([
[9, 10, 11, 12],
[11, 12, 13, 14]
])
# 横向和竖向拼接
numpy.vstack((n3, n4)) # [[1 2 3 4] [5 6 7 8] [9 10 11 12] [...]]
numpy.hstack((n3, n4)) # [ [1 2 3 4 9 10 11 12] [5 6 7 8 11 12 13 14]]
# numpy数组可以支持 a b = b a语法来换行或者换列,如
n2[[1, 2], :] = n2[[2, 1], :] # 行交换
n2[:, [0, 1]] = n2[:, [1, 0]] # 列交换

pandas

为什么需要pandas?

numpy能够帮我们处理数值型数据,但这样还不足以满足需求,有时候还需要处理字符串、时间序列等。不过pandas的数值处理模块也是基于numpy的。

什么是pandas?

pandas 是是python的一个数据分析包、基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。pandas提供了大量能使我们快速便捷地处理数据的函数和方法。对于金融行业的用户,pandas提供了大量适用于金融数据的高性能时间序列功能和工具。

pandas的数据结构:

  • Series:带标签(索引)的一维数组,与Numpy中的一维array类似。二者与Python基本的数据结构List也很相近,其区别是:List中的元素可以是不同的数据类型,而Array和Series中则只允许存储相同的数据类型,这样可以更有效的使用内存,提高运算效率。
  • Time- Series:以时间为索引的Series。
  • DataFrame:二维的表格型数据结构。很多功能与R中的data.frame类似。可以将DataFrame理解为Series的容器。
  • Panel :三维的数组,可以理解为DataFrame的容器。

pandas的相关用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# coding = 'utf-8'
import pandas as pd
import numpy as np

# Series相关

t1 = pd.Series([1, 2, 33, 44, 5])
t2 = pd.Series([1, 2, 3, 4, 5], index=list("abcde")) # 此时 1的索引为a,2的索引为b,以此类推
print(t2.dtype) # int64
temp_list = {"name": "q", "age": 26, "tel": "10086"}
t3 = pd.Series(temp_list) # 此时索引为字典的键,值为字典对应键的值
# tip:对一个series重新指定索引后,如果能够对应上,就取其值,如果不能,则为Nan
print(t3.dtype) # object

# Series的切片和索引
print(t3[:2]) # 取前3行
print(t3[["name", "age"]]) # 取name和age
print(t1[t1 > 2]) # 取值大于2的
print(t3.index) # 获取索引列表,类型为Index,可以使用list(t3.index) 强制转换为列表
print(t3.values) # 获取值的列表,类型为numpy.ndarry,其实就是一个数组
# tips:ndarry中很多方法在pandas中都可以用。但pandas中的where是不同的,可以用来查找符合条件的数值,而不符合的显示为指定的值,不指定则为Nan
print('*' * 20)

# DataFrame相关

p1 = pd.DataFrame(np.arange(12).reshape(3, 4))
print(p1)
# 带索引的创建,指定行索引和列索引
p2 = pd.DataFrame(np.arange(12).reshape(3, 4), index=list("abc"), columns=list("WXYZ"))
print(p2)
# 传入字典创建,键为列名,每一行为具体的值,值可以对应一个列表,列表内有多少数据就有多少行。也可以传入一个内容是字典的列表,有几个字典就有几行。没有的数据为NaN
d1 = {"name": ["q1", "q2"], "age": [25, 26], "tel": [10086, 10010]}
p3 = pd.DataFrame(d1)
print(p3)
d2 = [{"name": "q1", "age": 25, "tel": 10010}, {"name": "q2", "tel": 10000}, {"name": "q3", "age": 22, "tel": 10086}]
p4 = pd.DataFrame(d2)
print(p4)
# 前几行,tail为尾几行。info可以查看DataFrame的内存情况,多少行多少列占用多少内存等
p4.head(1)

# DataFrame取行取列,方括号内写数组,表示取行,写字符串,表示取列的索引
p5 = pd.DataFrame(np.arange(100).reshape(10, 10), index=["r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10"],
columns=["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10"])
# 取前5行
print(p5[:5])
# 取固定列
print(p5["c2"])
# 通过pandas优化过的选择方式:
# loc通过标签索引行数据,loc中冒号两边的数据取的方式是闭合区间[],而不是[)
# iloc通过位置获取行数据
print(p5.loc["r4", "c5"]) # 取r4行r5列
print(p5.loc["r4"]) # 取r4行
print(p5.loc[["r1", "r3"]]) # 取多行多列语法 如r1行和r3行
print(p5.iloc[:, 2]) # 取第2列,索引从0开始
print(p5.iloc[5:, 3:]) #取第6行以后和第3列以后的数据
print(p5[p5["c7"]>70]) #取c7列大于70的所有行

#缺失数据NaN的处理
p6 = pd.DataFrame(np.array([0,1,np.NaN,3,4,5,6,7,8,np.NaN,10,11]).reshape(3, 4), index=list("abc"), columns=list("WXYZ"))
print(p6)
print(pd.notnull(p6)) #p6中,不为NaN的值显示为true,为NaN的值显示为false
print(p6[pd.notnull(p6["X"])]) #选取p6的X列中的值不为NaN的所有行
print(p6.dropna(axis=0,how="any")) #删除DataFrame中值为NaN的axis,axis=0为行,1为列。how的值为“any”表示这个轴只要有NaN就删,值为“all”表示所有值都为NaN才删
#填充NaN为指定数据
print(p6.fillna(0)) # 将NaN填充为0
# 不过为了数据完整性,一般不会填充为0或者随便填一个数值,而是会填充为均值,Pandas计算均值的时候不会算上NaN值
print(p6.mean()) #此时mean算的均值是每一列的均值
print(p6.fillna(p6.mean()))
print(p6["X"].fillna(p6["X"].mean())) #只填充某一列的NaN值,如果要改变原数据,可以前面添加 p6["X"] =

# pandas读取外部数据
# csv文件
# result = pd.read_csv("./test.csv")
# 读取sql,传入sql语句和connection
# pd.read_sql(sql_sentence,connection)

写到这里

先上一张python数据分析的知识框架

其中有一些还没有写到,也不打算继续挖坑了,暂且记录在下面:

这段时间大概了解了一下python数据分析的知识框架,写的不是很详细,这里有一篇更详细的文章写得不错先mark一下:https://www.cnblogs.com/nxld/p/6058998.html

scrapy爬取腾讯招聘岗位到mongoDB中

前言

最近正值一年一度的春招时刻,看到一堆学弟学妹忙着准备简历和面试,默默地打开了腾讯的招聘网站,https://hr.tencent.com/position.php ,打算爬取一下所有的岗位信息,工作地点,类型,以及日期。

借此来复习一下Scrapy这个框架的基本使用,爬取的数据则是存储在本地的MongoDB数据库中。
Scrapy的架构图如下:

Scrapy主要包括了以下组件:

  • 引擎(Scrapy)
    • 用来处理整个系统的数据流, 触发事务(框架核心)
  • 调度器(Scheduler)
    • 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  • 下载器(Downloader)
    • 用于下载网页内容, 并将网页内容返回给spider(Scrapy下载器是建立在twisted这个高效的异步模型上的)
  • 爬虫(Spiders)
    • 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
  • 项目管道(Pipeline)
    • 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
  • 下载器中间件(Downloader Middlewares)
    • 位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
  • 爬虫中间件(Spider Middlewares)
    • 介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
  • 调度中间件(Scheduler Middewares)
    • 介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

Scrapy运行流程大概如下:

  • 引擎从调度器中取出一个链接(URL)用于接下来的抓取
  • 引擎把URL封装成一个请求(Request)传给下载器
  • 下载器把资源下载下来,并封装成应答包(Response)
  • 爬虫解析Response
  • 解析出实体(Item),则交给实体管道进行进一步的处理
  • 解析出的是链接(URL),则把URL交给调度器等待抓取

建立项目

假设已经安装好mongodb数据库和scrapy。
windows下,cd到对应的文件夹目录

python
1
2
3
scrapy startproject myspiders
#cd到 myspiders 文件夹下
scrapy genspider myspider tencent.com

这之后在项目的spiders文件夹下,会生成一个 myspider.py 的文件,修改里面的 start_urls为我们的起始地址 https://hr.tencent.com/position.php
settings.py文件中,去掉 ITEM_PIPELINES 的注释,以使pipelines.py文件可以使用
settings.py中添加一行log等级,过滤一下多余log,LOG_LEVEL = “WARNING”
settings.py中的USER_AGENT取消注释,并改为自己浏览器的useragent

Request参数参考:scrapy.http.Request(url[, callback, method=’GET’, headers, body, cookies, meta, encoding=’utf-8’, priority=0, dont_filter=False, errback])

myspider.py代码如下:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# -*- coding: utf-8 -*-
import scrapy


class MyspiderSpider(scrapy.Spider):
name = 'myspider'
allowed_domains = ['tencent.com']
start_urls = ['https://hr.tencent.com/position.php']

def parse(self, response):
tr_list = response.xpath("//table[@class='tablelist']/tr")[1:-1]
for tr in tr_list:
item={}
item["title"] = tr.xpath("./td[1]/a/text()").extract_first()
item["type"] = tr.xpath("./td[2]/text()").extract_first()
item["position"] = tr.xpath("./td[4]/text()").extract_first()
item["date"] = tr.xpath("./td[5]/text()").extract_first()
yield item

#找下一页地址
next_url = response.xpath("//a[@id='next']/@href").extract_first()
if next_url != "javascript:;":
next_url = "http://hr.tencent.com/"+next_url
yield scrapy.Request(
next_url,
callback=self.parse
)

pipelines.py如下:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html

from pymongo import MongoClient

client = MongoClient(host="127.0.0.1",port=27017)
collection = client["tencent"]["job"]

class MyspidersPipeline(object):
def process_item(self, item, spider):
print(item)
collection.insert(item)
return item

写完之后返回命令行,运行

python
1
scrapy crawl myspider

程序结果,MongoDB数据库中的结果如下:

写在后面

scrapy这个框架极其强大,它也提供了多种类型爬虫的基类,如BaseSpider、sitemap爬虫等,这里只是用到了最基本的几个用法。之后有机会借用其他例子来更深入理解。

多线程爬虫

多线程爬取糗事百科段子

上一篇中有一个爬取糗事百科段子的demo,但是如果需要请求的url太多的情况下,一个一个请求肯定会很慢,影响效率,而且耗时主要是在网络请求中,属于IO密集型的代码,所以GIL锁在这里的影响不会很大。

主要用到了threading模块和queue相关的内容,而这部分内容网上一搜一大把- -不多解释。

以下为改写为多线程的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# coding=utf-8
import requests
from lxml import html
import threading
from queue import Queue
import os

class QuibaiSpdier:
def __init__(self):
self.url_temp = "https://www.qiushibaike.com/hot/page/{}/"
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36"}
self.url_queue = Queue() # 需要请求的url地址
self.html_queue = Queue()
self.content_queue = Queue()

def get_url_list(self):
# return [self.url_temp.format(i) for i in range(1, 14)]
for i in range(1, 14):
self.url_queue.put(self.url_temp.format(i))

def parse_url(self):
while True:
url = self.url_queue.get()
print("request:" + url)
response = requests.get(url, headers=self.headers)
# return response.content.decode()
self.html_queue.put(response.content.decode())
self.url_queue.task_done()

def get_content_list(self): # 提取数据
while True:
html_str = self.html_queue.get()
html_elements = html.etree.HTML(html_str)
div_list = html_elements.xpath("//div[@id='content-left']/div")
content_list = []
for div in div_list:
item = {}
item["content"] = div.xpath(".//div[@class='content']/span/text()")
item["author_gender"] = div.xpath(".//div[contains(@class,'articleGender')]/@class")
item["author_gender"] = item["author_gender"][0].split(" ")[-1].replace("Icon", "") if len(
item["author_gender"]) > 0 else None
item["content_img"] = div.xpath(".//div[@class='thumb']/a/img/@src")
item["content_img"] = "https:" + item["content_img"][0] if len(item["content_img"]) > 0 else None
item["author_img"] = div.xpath(".//div[@class='author clearfix']//img/@src")
item["author_img"] = "https:" + item["author_img"][0] if len(item["author_img"]) > 0 else None
item["stats_vote"] = div.xpath(".//span[@class='stats-vote']/i/text()")
item["stats_vote"] = item["stats_vote"][0] if len(item["stats_vote"]) > 0 else None
content_list.append(item)
# return content_list
self.content_queue.put(content_list)
self.html_queue.task_done()

def save_content_list(self): # 保存数据
while True:
content_list = self.content_queue.get()
with open("qiubai.txt", "a", encoding='utf-8') as f:
for content in content_list:
# f.write(json.dumps(content, ensure_ascii=False))
f.write("\n".join(content["content"]) + "点赞数:" + content["stats_vote"])
f.write("\n")
self.content_queue.task_done()

def run(self): # 实现主要逻辑
thread_list = []
# 1.构造url_list
# url_list = self.get_url_list()
t_geturl = threading.Thread(target=self.get_url_list)
thread_list.append(t_geturl)
# 2.遍历list,发送请求获取相应
for i in range(7): # 来7个线程来请求url地址
t_parseurl = threading.Thread(target=self.parse_url)
thread_list.append(t_parseurl)
# for url in url_list:
# html_str = self.parse_url(url)
# 3.提取数据
# content_list = self.get_content_list(html_str)
for i in range(3):
t_getcontent = threading.Thread(target=self.get_content_list)
thread_list.append(t_getcontent)
# 4.保存数据
# self.save_content_list(content_list)
t_save = threading.Thread(target=self.save_content_list)
thread_list.append(t_save)
for t in thread_list:
t.setDaemon(True) # 子线程设置为守护线程,表示该线程不重要,主线程结束则子线程也会结束
t.start()
for q in [self.url_queue, self.html_queue, self.content_queue]:
q.join() # 阻塞等待子线程结束
print("主线程结束")


if __name__ == '__main__':
if os.path.exists("qiubai.txt"):
os.remove("qiubai.txt")
qiubai = QuibaiSpdier()
qiubai.run()

爬虫基础

前面http和https的介绍,可直接跳过看下面的案例

HTTP与HTTPS

为什么要先简要复习一下http和https?因为要发送请求,模拟浏览器,获取和浏览器一模一样的响应。

HTTP:超文本传输协议,默认端口:80,是一种建立在TCP上的无状态连接,说白了就是个协议,规定了一些列的规则,整个基本的工作流程是客户端发送一个HTTP请求,说明客户端想要访问的资源和

请求的动作,服务端收到请求之后,服务端开始处理请求,并根据请求做出相应的动作访问服务器资源,最后通过发送HTTP响应把结果返回给客户端。

HTTPS:即HTTP+SSL(安全套接字层),默认端口号:443,即在HTTP上多了个安全套接字层SSL,SSL是一个提供数据安全和完整性的协议,负责网络连接的加密。    

题外话:HTTPS通信中的几个概念:加密分为对称和非对称
对称加密:信息的加密和解密都是通过同一个密钥进行的,实际通信中,一个服务器可以同时对应好几个客户端,也就是A客户端获得的加解密算法同样可以加解密B客户端
的消息,这跟没加密一样,为了防止这种情况,就只能不同的客户端使用不同密钥,这样的话密钥将会有很多,并且在通信刚开始,服务器和A客户端就要协商好用什么密钥,而这个协商过程是不能加密
的,不然A客户端就读不懂服务器消息了,因此还是存在风险。

非对称加密:应用最广的加密机制“非对称加密”,特点是私钥加密后的密文,只要是公钥,都可以解密,但是反过来公钥加密后的密文,只有私钥可以解密。私钥只有一个
人有,而公钥可以发给所有的人。所以公钥不需要加密,而私钥只存在于服务器。这样只需要一套公钥和私钥就可以了。那么现在的问题是,如何让客户端安全的获取公钥?如果服务器直接明文发送给客户
端,那么可能发生被劫持的情况,例如客户端A和服务器S通信,S给A的公钥被B劫持后修改了,那么之后A将会用假的公钥进行加密,将消息发给服务器时B继续劫持,查看内容或者修改之后用真公钥加密然
后发给服务器,这就是中间人攻击。这时候风险依然存在,而为了解决这个问题,采用了一种SSL 证书(需要购买)和CA机构的方法,涉及防伪和证书链的概念,大概流程是:
在客户端第一次请求服务器时,服务器发送回一个SSL证书给客户端,SSL 证书中包含的具体内容有证书的颁发机构的证书、有效期、公钥、证书持有者、签名。防伪的步骤:浏览器
拿到这个证书后读取证书中的证书所有者、有效期等信息进行一一校验,开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁
发,如果没找到,则报错,如果找到了,浏览器会从操作系统中取出颁发者CA的公钥,然后对服务器发来的证书里面的签名进行解密,如果能够解密则一定是证书颁发机构颁发的证书如果解密后信息
与Server信息一致则确实是颁发给该Server的,综上校验通过,这就是防伪的流程,而证书链的作用可以保证正在通信的Server确实是证书颁发机构指定的Server,流程是:通过和Server证书验证同
的过程通过内嵌在浏览器或者JDK中的根证书验证下中级证书的合法性就好了。因为根证书是内嵌的具有绝对的合法性,如果根证书信任该中级机构,则该中级证书颁发的证书也是可信的这就是证书链了。
这样通过第三方的校验保证了身份的合法,解决了公钥获取的安全性。什么你问我怎么申请CA证书?国内的阿里云和腾讯云上找去

举个栗子:爬取某贴吧前1000页内容

先来一个简单的例子熟悉一下api和爬虫基本流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import requests

class TiebaSpider:
def __init__(self, tieba_name):
self.tieba_name = tieba_name
self.url_temp = "https://tieba.baidu.com/f?kw=" + tieba_name + "&ie=utf-8&pn={}"
self.headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36"}

def get_url_list(self): #构造url列表
# url_list = []
# for i in range(1000):
# url_list.append(self.url_temp.format(i*50))
# return url_list
return [self.url_temp.format(i*50) for i in range(1000)]

def parse_url(self, url): #发送请求获取相应
print(url)
response = requests.get(url, headers=self.headers)
return response.content.decode()

def save_html(self, html_str, page_num):
file_path = "{}第{}页.html".format(self.tieba_name, page_num)
with open(file_path, "w",encoding="utf-8") as f:
f.write(html_str)

def run(self):
# 构造url列表
url_list = self.get_url_list()
# 遍历,发送请求,获取响应
for url in url_list:
html_str = self.parse_url(url)
page_num = url_list.index(url)+1
self.save_html(html_str, page_num)
# 保存


if __name__ == '__main__':
tieba_spider = TiebaSpider("李毅")
tieba_spider.run()

这里爬取下来的内容是没问题的,但保存下来的html直接打开是有问题的,因为万恶的百度在其中把大量有用内容添加了注释,后续是通过js来去掉注释的。这里暂时不处理,只展示基本api的使用。

发送POST请求

什么时候需要发送post请求?

  • 登录注册等有敏感信息的时候,POST比GET更安全
  • 需要传输大文本的时候(POST请求对数据长度没有要求)
    python
    1
    response = requests.post("",data=data,headers=headers)

使用代理

为什么爬虫需要代理?

  • 让服务器以为不是同一个客户端在不断请求
  • 防止我们真实地址倍泄露,防止被追究
python
1
response = requests.get("url",proxies=proxies)

proxies是一个字典,

1
2
3
4
proxies={
"http":"http://xx.xx.xx.xx:xxx",
"https":"https://xx.xx.xx.xx:xxxx",
}

request模拟登陆

cookie和session区别:

  • cookie数据存放在客户的浏览器上,session数据存放在服务器上
  • cookie不安全,别人可以分析存放在本地的cookie并进行cookie欺骗
  • session会在一定时间内保存在服务器上,当访问增多,会比较占用服务器性能
  • 单个cookie保存的数据不能超过4k,很多浏览器会限制一个站点最多保存20个cookie

1)request提供了一个session类,来实现客户端和服务器的会话保持,先实例化一个session,用session发送post请求登陆网站,把cookie保存在session中,再使用session请求登陆之后才能访问的网站,session能够自动的携带登陆成功时保存在其中的cookie。
使用方法:

1
2
session = requests.session()
response = session.get(url,headers)

2)如果请求页面时不发送post请求的情况下,可以在headers中添加cookie键值对,要注意cookie的有效时间

3)也可以把cookie作为request的参数。此时cookie参数时一个字典。

request小技巧

  • request.utils.dict_from_cookiejar(response.cookies) 把cookie转换为字典,request.utils.cookiejar_from_dict ,将字典转化为cookie
  • request.utils.unquote(“编码后的url”) 将url地址解码,反之quote为编码
  • 忽视证书错误 request.get(“url”,verify=false)
  • 设置超时 request.get(“url”,timeout)
  • 刷新网页 使用第三方模块retrying,还可以定义最大重新请求次数

爬虫的基本套路

  • 准备url

    • 准备start_url
      • url地址规律不明显,总数不确定的情况下
      • 通过代码提取下一页的url地址
        • 当下一页的地址在网页的响应中时,可以通过xpath
        • 通过其他方式寻找url地址,比如通过js生成,这种情况下部分参数是在当前响应中
    • 准备url_list
      • 页码总数明显的时候
      • url地址规律很明显
  • 发送请求,获取响应

    • 添加随机的User-Agent,防反爬虫
    • 添加随机的代理ip,防反爬虫
    • 在对方判断出是爬虫之后,应该添加更多的header字段,包括cookies
    • cookie可以用session来解决
    • 如果不登录的话
      • 准备能成功请求对方网站的cookie,即接收对方网站设置在response的cookie
      • 下次请求的时候,使用之前的列表中的cookie来请求
    • 如果登录的话
      • 准备多个账号
      • 获取多个账号cookie
      • 之后随机选择cookie来请求登录之后才能访问的网站
  • 提取数据

    • 确定数据位置,确定数据是否在当前的url地址响应中
      • 如果在当前url地址响应中
        • 提取的是列表页的数据
          • 直接请求列表页的url地址,不需要进入详情页
        • 提取的是详情页的数据
          • 1.确定详情页url地址
          • 2.发送请求
          • 3.提取数据
          • 4.返回
      • 如果不在当前url响应中
        • 在其他响应中,寻找数据位置
    • 数据的提取
      • xpath,从html中提取整块的数据,先分组,然后针对每一组进行提取
      • json
      • re,提取html中的json字符串或者某些容易用正则区分出的属性等
  • 保存数据

一个小案例,爬去豆瓣上最近热播的英美剧,国产剧,动漫以及综艺的节目名称和评分

网页截图以及爬去后的效果图如下:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# coding=utf-8
import requests
import json

class DoubanSpider:
def __init__(self):
self.headers = {
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
"Referer": "https://m.douban.com/tv/american"}
self.url_temp_list = [
{
"url_temp": "https://m.douban.com/rexxar/api/v2/subject_collection/tv_american/items?start{}&count=18&loc_id=108288",
"type": "英美剧",
},
{
"url_temp": "https://m.douban.com/rexxar/api/v2/subject_collection/tv_domestic/items?start{}&count=18&loc_id=108288",
"type": "国产剧",
},
{
"url_temp": "https://m.douban.com/rexxar/api/v2/subject_collection/tv_animation/items?start{}&count=18&loc_id=108288",
"type": "动漫",
},
{
"url_temp": "https://m.douban.com/rexxar/api/v2/subject_collection/tv_variety_show/items?start{}&count=18&loc_id=108288",
"type": "综艺",
}
]
self.curIndex=1

def parse_url(self, url):
print(url)
response = requests.get(url, headers=self.headers)
return response.content.decode()

def get_content_list(self, json_str):
dict_ret = json.loads(json_str)
content_list = dict_ret["subject_collection_items"]
total = dict_ret["total"]
return content_list, total

def save_content_list(self, content_list):
with open("douban.txt", "a", encoding='utf-8') as f:
for content in content_list:
#f.write(json.dumps(content, ensure_ascii=False))
f.write(self.curIndex.__str__()+" :" + content["title"]+" 评分:"+content["rating"]["value"].__str__())
f.write("\n")
self.curIndex += 1

def run(self): # 主逻辑
for url_temp in self.url_temp_list:
with open("douban.txt", "a", encoding='utf-8') as f:
f.write(url_temp["type"] + ":================================================================") # 分割线,方便数据查看
f.write("\n")
self.curIndex = 1
num = 0
total = 1 # 假设刚开始的条件成立
while num < total + 18:
# 1.构造一个start_rul
url = url_temp["url_temp"].format(num)
# 2.发送请求,获取响应
json_str = self.parse_url(url)
# 3.提取数据,保存
content_list, total = self.get_content_list(json_str)
self.save_content_list(content_list)
# if len(content_list < 18):
# break
# 4,构造下一页的url地址,进入2,3循环
num += 18


if __name__ == '__main__':
douban = DoubanSpider()
douban.run()
print("complete")

2019-02-25 更新

通用案例之糗事百科段子

作为一名段子手- -不应该只会刷段子,还要学会爬取。。
其中用到了 lxml模块和xpath相关内容,爬取到的信息不止有文字内容,还有图片以及作者名字和性别,但此处只在txt文档里放了段子内容。
效果图如下:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# coding=utf-8
import requests
from lxml import html


class QuibaiSpdier:
def __init__(self):
self.url_temp = "https://www.qiushibaike.com/hot/page/{}/"
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36"}

def get_url_list(self):
return [self.url_temp.format(i) for i in range(1, 14)]

def parse_url(self, url):
response = requests.get(url, headers=self.headers)
return response.content.decode()

def get_content_list(self, html_str):
html_elements = html.etree.HTML(html_str)
div_list = html_elements.xpath("//div[@id='content-left']/div")
content_list = []
for div in div_list:
item = {}
item["content"] = div.xpath(".//div[@class='content']/span/text()")
item["author_gender"] = div.xpath(".//div[contains(@class,'articleGender')]/@class")
item["author_gender"] = item["author_gender"][0].split(" ")[-1].replace("Icon", "") if len(
item["author_gender"]) > 0 else None
item["content_img"] = div.xpath(".//div[@class='thumb']/a/img/@src")
item["content_img"] = "https:" + item["content_img"][0] if len(item["content_img"]) > 0 else None
item["author_img"] = div.xpath(".//div[@class='author clearfix']//img/@src")
item["author_img"] = "https:" + item["author_img"][0] if len(item["author_img"]) > 0 else None
item["stats_vote"] = div.xpath(".//span[@class='stats-vote']/i/text()")
item["stats_vote"] = item["stats_vote"][0] if len(item["stats_vote"]) > 0 else None
content_list.append(item)
return content_list

def save_content_list(self, content_list):
with open("qiubai.txt", "a", encoding='utf-8') as f:
for content in content_list:
# f.write(json.dumps(content, ensure_ascii=False))
f.write("\n".join(content["content"]) + "点赞数:" + content["stats_vote"])
f.write("\n")

def run(self): # 实现主要逻辑
# 1.构造url_list
url_list = self.get_url_list()
# 2.遍历list,发送请求获取相应
for url in url_list:
html_str = self.parse_url(url)
# 3.提取数据
content_list = self.get_content_list(html_str)
# 4.保存数据
self.save_content_list(content_list)


if __name__ == '__main__':
qiubai = QuibaiSpdier()
qiubai.run()