第一部分:使用sklearn构建完整的分类项目
步骤1:收集数据集并选择合适的特征:
在数据集上我们使用我们比较熟悉的IRIS鸢尾花数据集。
from sklearn import datasets iris = datasets.load_iris() X = iris.data y = iris.target feature = iris.feature_names data = pd.DataFrame(X,columns=feature) data['target'] = y data.head()
各个特征的相关解释:
sepal length (cm):花萼长度(厘米)
sepal width (cm):花萼宽度(厘米)
petal length (cm):花瓣长度(厘米)
petal width (cm):花瓣宽度(厘米)
步骤2:选择度量模型性能的指标:
度量分类模型的指标和回归的指标有很大的差异:
(1)首先是因为分类问题本身的因变量是离散变量,因此像定义回归的指标那样,单单衡量预测值和因变量的相似度可能行不通。
(2)其次,在分类任务中,我们对于每个类别犯错的代价不尽相同,例如:我们将癌症患者错误预测为无癌症和无癌症患者错误预测为癌症患者,在医院和个人的代价都是不同的,前者会使得患者无法得到及时的救治而耽搁了最佳治疗时间甚至付出生命的代价,而后者只需要在后续的治疗过程中继续取证就好了,因此我们很不希望出现前者,当我们发生了前者这样的错误的时候会认为建立的模型是很差的。为了解决这些问题,我们必须将各种情况分开讨论,然后给出评价指标。
真阳性TP:预测值和真实值都为正例;
真阴性TN:预测值与真实值都为正例;
假阳性FP:预测值为正,实际值为负;
假阴性FN:预测值为负,实际值为正;
分类模型的指标:
在本次小案例中,我们使用ROC曲线作为最终评价指标。
步骤3:选择具体的模型并进行训练
一、逻辑回归logistic regression:
说到分类问题与回归问题的区别,在于回归问题与分类问题需要预测的因变量不一样。在回归问题中,因变量是连续性变量,我们需要预测
E ( Y ∣ X ) E(Y|X)
E(Y∣X)
是一个连续的实数,但是在分类问题中,我们往往是通过已知X的信息预测Y的类别,往往是一个离散集合中的某个元素。如:是否患癌症,图片是猫还是狗等。
一个很自然的想法是能否用线性回归去处理分类问题,答案是可以但不好!先来看看线性回归处理分类问题会出现什么弊端,我们仔细来看这个线性回归的例子,
,只要输入Balance 和 Income 以及default的数据就能用最小二乘法估计出β 0 , β 1 {\beta_0,\beta_1}β
0
,β
1
,设定预测的default>0.5就是违约反之不违约,感觉很完美的样子,但事实真的是这样吗?假设我们需要用某个人的债务(Balance)和收入(Income)去预测是否会信用卡违约(default):
我们假设有一个穷人Lisa,他的Balance和Income都很小,那么有可能会导致default的值为负数,那么这个负数代表什么意义呢?显然是没有任何意义的。
当我们的分类变量是多类的时候,以0.5为界限划分分类就不可用了,那么我们应该怎么找到一个界限衡量多分类呢?
基于以上问题,现在大家是否还觉得线性回归模型作为一个分类模型是否足够优秀呢?其实,为了解决以上的问题(1)我们来想想能不能将线性回归的结果default转化为区间[0:1]上,让default转变成一个违约的概率呢?下面我们来解决这个问题吧。
在推导逻辑回归之前,我们先来认识下一组函数,这组函数可以将是实数轴上的数转换为[0:1]区间上的概率。
首先,我们假设我们的线性回归模型为
因此,
由于这里涉及的函数不像线性回归一样能简单求出解析解,因此我们使用迭代的优化算法:梯度下降法,即:
# 逻辑回归 ''' penalty {‘l1’, ‘l2’, ‘elasticnet’, ‘none’}, default=’l2’正则化方式 dual bool, default=False 是否使用对偶形式,当n_samples> n_features时,默认dual = False。 C float, default=1.0 solver {‘newton-cg’, ‘lbfgs’, ‘liblinear’, ‘sag’, ‘saga’}, default=’lbfgs’ l1_ratio float, default=None ''' from sklearn.linear_model import LogisticRegression log_iris = LogisticRegression() log_iris.fit(X,y) log_iris.score(X,y) # 0.9733333333333334
如果想了解关于梯度下降法等无约束算法的具体细节,可以参照萌弟大佬写的另外两篇知乎博客:
最优化理论之无约束优化基本结构及其python应用:https://zhuanlan.zhihu.com/p/163405865
最优化理论之负梯度方法与Newton型方法:https://zhuanlan.zhihu.com/p/165914126
对于问题(2),我们值得注意的是,逻辑回归在实际中不太用于多分类问题,因为实际效果不是很好,所以我们可以借助其他模型来解决这个问题,那让我们来解决这个遗留下来的问题吧。
二、基于概率的分类模型:
(1) 线性判别分析:
线性判别分析是一个比较久远的算法,下面将会从两个方向去描述这个算法,分别是基于贝叶斯公式和降维分类的思想。
角度一:基于贝叶斯公式:
在讨论如何解决多分类问题之前,我们先来说说贝叶斯的那些事吧。在概率统计的领域里有一条神奇的公式叫贝叶斯定理,具体的形式是:
我们先不要被公式的符号吓到,我们先来看看符号具体代表什么意思。我们假设观测有K {K}K类,π k {\pi_k}π
k
为随机选择的观测来自第k {k}k类的 先验概率,也就是样本里面第k {k}k类的样本个数除以总样本的个数:
我们推到出了一个自变量的简单模型,就要泛化为多个自变量的线性判别分析了,即p > 1 {p>1}p>1。其实原理一样的,只是将一元正态分布扩展为多元正态分布:
角度二:基于降维分类的思想:
基于数据进行分类时,一个很自然的想法是:将高维的数据降维至一维,然后使用某个阈值将各个类别分开。下面用图的形式展示:
图中,数据的维度是二维的,我们的想法是把数据降维至一维,然后用阈值就能分类。这个似乎是一个很好的想法,我们总是希望降维后的数据同一个类别自身内部方差小,不同类别之间的方差要尽可能大。这也是合理的,因为同一个类别的数据应该更加相似,因此方差小;不同类别的数据之间应该很不相似,这样才能更容易对数据进行分类,我们简称为:类内方差小,类间方差大,在计算机语言叫“松耦合,高内聚”。在做具体的推导之前,我们对数据的形式和一些基本统计量做一些描述:
∈{+1,−1},类别c1的特征
# 线性判别分析 ''' 参数: solver:{'svd','lsqr','eigen'},默认='svd' solver的使用,可能的值: 'svd':奇异值分解(默认)。不计算协方差矩阵,因此建议将此求解器用于具有大量特征的数据。 'lsqr':最小二乘解,可以与收缩结合使用。 'eigen':特征值分解,可以与收缩结合使用。 ''' from sklearn.discriminant_analysis import LinearDiscriminantAnalysis lda_iris = LinearDiscriminantAnalysis() lda_iris.fit(X,y) lda_iris.score(X,y) # 0.98
(2) 朴素贝叶斯:
在线性判别分析中,我们假设每种分类类别下的特征遵循同一个协方差矩阵,每两个特征之间是存在协方差的,因此在线性判别分析中各种特征是不是独立的。但是,朴素贝叶斯算法对线性判别分析作进一步的模型简化,它将线性判别分析中的协方差矩阵中的协方差全部变成0,只保留各自特征的方差,也就是朴素贝叶斯假设各个特征之间是不相关的。
在之前所看到的偏差-方差理论中,我们知道模型的简化可以带来方差的减少但是增加偏差,因此朴素贝叶斯也不例外,它比线性判别分析模型的方差小,偏差大。虽然简化了模型,实际中使用朴素贝叶斯的案例非常多,甚至多于线性判别分析,例如新闻分类,垃圾邮件分类等。
# 朴素贝叶斯 from sklearn.naive_bayes import GaussianNB NB_iris = GaussianNB() NB_iris.fit(X, y) NB_iris.score(X,y) # 0.96
三、决策树 :
与前面内容所讲的决策树回归大致是一样的,只是在回归问题中,选择分割点的标准是均方误差,但是在分类问题中,由于因变量是类别变量而不是连续变量,因此用均方误差显然不合适。那问题是用什么作为选择分割点的标准呢?我们先来分析具体的问题:
在回归树中,对一个给定的观测值,因变量的预测值取它所属的终端结点内训练集的平均因变量。与之相对应,对于分类树来说,给定一个观测值,因变量的预测值为它所属的终端结点内训练集的最常出现的类。分类树的构造过程与回归树也很类似,与回归树一样,分类树也是采用递归二叉分裂。但是在分类树中,均方误差无法作为确定分裂节点的准则,一个很自然的替代指标是分类错误率。分类错误率就是:此区域内的训练集中非常见类所占的类别,即:
上式中的p ^ m k \hat{p}_{mk}
p
^
mk
代表第m个区域的训练集中第k类所占的比例。但是在大量的事实证明:分类错误率在构建决策树时不够敏感,一般在实际中用如下两个指标代替:
(1) 基尼系数:
在基尼系数的定义中,我们发现这个指标衡量的是K个类别的总方差。不难发现,如果所有的p ^ m k \hat{p}_{mk}
p
^
mk
的取值都接近0或者1,基尼系数会很小。因此基尼系数被视为衡量结点纯度的指标----如果他的取值小,那就意味着某个节点包含的观测值几乎来自同一个类别。
由基尼系数作为指标得到的分类树叫做:CART。
(2) 交叉熵:
可以替代基尼系数的指标是交叉熵,定义如下:
显然,如果所有的p ^ m k \hat{p}_{mk}
p
^
mk
都接近于0或者1,那么交叉熵就会接近0。因此,和基尼系数一样,如果第m个结点的纯度越高,则交叉熵越小。事实证明,基尼系数和交叉熵在数值上时很接近的。
决策树分类算法的完整步骤:
a. 选择最优切分特征 j jj 以及该特征上的最优点 s ss:
遍历特征 j jj 以及固定j后遍历切分点s,选择使得基尼系数或者交叉熵最小的 ( j , s ) (j,s)(j,s)
b. 按照 ( j , s ) (j,s)(j,s) 分裂特征空间,每个区域内的类别为该区域内样本比例最多的类别。
c. 继续调用步骤1,2直到满足停止条件,就是每个区域的样本数小于等于5。
d. 将特征空间划分为 J JJ 个不同的区域,生成分类树。
# 使用决策树算法对iris分类: ''' criterion:{“gini”, “entropy”}, default=”gini” max_depth:树的最大深度。 min_samples_split:拆分内部节点所需的最少样本数 min_samples_leaf :在叶节点处需要的最小样本数。 ''' from sklearn.tree import DecisionTreeClassifier tree_iris = DecisionTreeClassifier(min_samples_leaf=5) tree_iris.fit(X,y) tree_iris.score(X,y) # 0.9733333333333334
四、支持向量机SVM:
支持向量机SVM是20世纪90年代在计算机界发展起来的一种分类算法,在许多问题中都被证明有较好的效果,被认为是适应性最广的算法之一。
支持向量机的基本原理非常简单,如图所视,白色和蓝色的点各为一类,我们的目标是找到一个分割平面将两个类别分开。通常来说,如果数据本身是线性可分的,那么事实上存在无数个这样的超平面。这是因为给定一个分割平面稍微上移下移或旋转这个超平面,只要不接触这些观测点,仍然可以将数据分开。一个很自然的想法就是找到最大间隔超平面,即找到一个分割平面距离最近的观测点最远。下面我们来严格推导:
我们根据距离超平米那最近的点,只要同时缩放w和b可以得到
五、非线性支持向量机:
在刚刚的讨论中,我们都是着重讨论了线性支持向量机是如何工作的,但是在现实生活中,我们很难碰到线性可分的数据集,如:
那我们应该如何处理非线性问题呢?——将数据投影至更加高的维度!
上图中,在一维数据做不到线性可分,我们将数据投影至二维平面就可以成功线性可分。那么,我们来详细探讨下这其中的奥妙:
如果我们使用上面公式的形式将低维数据拓展至高维数据,则必须面临一个很大的问题,那就是:维度爆炸导致的计算量太大的问题。假如是一个2维特征的数据,我们可以将其映射到5维来做特征的内积,如果原始空间是三维,可以映射到到19维空间,似乎还可以处理。但是如果我们的低维特征是100个维度,1000个维度呢?
那么我们要将其映射到超级高的维度来计算特征的内积。这时候映射成的高维维度是爆炸性增长的,这个计算量实在是太大了,而且如果遇到无穷维的情况,就根本无从计算了。能不能呢个避免这个问题呢?核函数隆重登场:
回顾线性可分SVM的优化目标函数:
注意到上式低维特征仅仅以内积x i ∙ x j x_i \bullet x_jx
i
∙x
j
的形式出现,如果我们定义一个低维特征空间到高维特征空间的映射ϕ \phiϕ,将所有特征映射到一个更高的维度,让数据线性可分,我们就可以继续按前两篇的方法来优化目标函数,求出分离超平面和分类决策函数了。也就是说现在的SVM的优化目标函数变成:
可以看到,和线性可分SVM的优化目标函数的区别仅仅是将内积
替换为
核函数
下面引入核函数:
假设ϕ \phiϕ是一个从低维的输入空间χ \chiχ(欧式空间的子集或者离散集合)到高维的希尔伯特空间的H \mathcal{H}H映射。那么如果存在函数K ( x , z ) K(x,z)K(x,z),对于任意x , z ∈ χ x, z \in \chix,z∈χ,都有:
那么我们就称K ( x , z ) K(x, z)K(x,z)为核函数。
仔细发现,K ( x , z ) K(x, z)K(x,z)的计算是在低维特征空间来计算的,它避免了在刚才我们提到了在高维维度空间计算内积的恐怖计算量。也就是说,我们可以好好享受在高维特征空间线性可分的利益,却避免了高维特征空间恐怖的内积计算量。下面介绍几种常用的核函数:
(1) 多项式核函数:
多项式核函数(Polynomial Kernel)是线性不可分SVM常用的核函数之一,表达式为:
C用来控制低阶项的强度,C=0,d=1代表无核函数。
(2) 高斯核函数:
高斯核函数(Gaussian Kernel),在SVM中也称为径向基核函数(Radial Basis Function,RBF),它是非线性分类SVM最主流的核函数。libsvm默认的核函数就是它。表达式为:
使用高斯核函数之前需要将特征标准化,因此这里衡量的是样本之间的相似度。
(3) Sigmoid核函数:
Sigmoid核函数(Sigmoid Kernel)也是线性不可分SVM常用的核函数之一,表达式为:
此时的SVM相当于没有隐藏层的简单神经网络。
(4) 余弦相似度核:
常用于衡量两段文字的余弦相似度,表达式为:
from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC ''' C:正则化参数。正则化的强度与C成反比。必须严格为正。惩罚是平方的l2惩罚。 kernel:{'linear','poly','rbf','sigmoid','precomputed'},默认='rbf' degree:多项式和的阶数 gamma:“ rbf”,“ poly”和“ Sigmoid”的内核系数。 shrinking:是否软间隔分类,默认true ''' svc_iris = make_pipeline(StandardScaler(), SVC(gamma='auto')) svc_iris.fit(X, y) svc_iris.score(X,y) # 0.9733333333333334
步骤4:评估模型的性能并调参:
更详细的可以查看萌弟大佬的知乎:https://zhuanlan.zhihu.com/p/140040705
(1)方式1:网格搜索GridSearchCV()
# 使用网格搜索进行超参数调优: # 方式1:网格搜索GridSearchCV() from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC import time start_time = time.time() pipe_svc = make_pipeline(StandardScaler(),SVC(random_state=1)) param_range = [0.0001,0.001,0.01,0.1,1.0,10.0,100.0,1000.0] param_grid = [{'svc__C':param_range,'svc__kernel':['linear']},{'svc__C':param_range,'svc__gamma':param_range,'svc__kernel':['rbf']}] gs = GridSearchCV(estimator=pipe_svc,param_grid=param_grid,scoring='accuracy',cv=10,n_jobs=-1) gs = gs.fit(X,y) end_time = time.time() print("网格搜索经历时间:%.3f S" % float(end_time-start_time)) print(gs.best_score_) print(gs.best_params_)
网格搜索经历时间:4.300 S 0.9800000000000001 {'svc__C': 1.0, 'svc__gamma': 0.1, 'svc__kernel': 'rbf'}
(2)方式2:随机网格搜索RandomizedSearchCV()
# 方式2:随机网格搜索RandomizedSearchCV() from sklearn.model_selection import RandomizedSearchCV from sklearn.svm import SVC import time start_time = time.time() pipe_svc = make_pipeline(StandardScaler(),SVC(random_state=1)) param_range = [0.0001,0.001,0.01,0.1,1.0,10.0,100.0,1000.0] param_grid = [{'svc__C':param_range,'svc__kernel':['linear']},{'svc__C':param_range,'svc__gamma':param_range,'svc__kernel':['rbf']}] # param_grid = [{'svc__C':param_range,'svc__kernel':['linear','rbf'],'svc__gamma':param_range}] gs = RandomizedSearchCV(estimator=pipe_svc, param_distributions=param_grid,scoring='accuracy',cv=10,n_jobs=-1) gs = gs.fit(X,y) end_time = time.time() print("随机网格搜索经历时间:%.3f S" % float(end_time-start_time)) print(gs.best_score_) print(gs.best_params_)
随机网格搜索经历时间:0.942 S 0.9733333333333334 {'svc__kernel': 'linear', 'svc__C': 100.0}
(3)绘图
当类别为两类时,可以绘制混淆矩阵与ROC曲线
混淆矩阵:
# 混淆矩阵: # 加载数据 df = pd.read_csv("http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data",header=None) ''' 乳腺癌数据集:569个恶性和良性肿瘤细胞的样本,M为恶性,B为良性 ''' # 做基本的数据预处理 from sklearn.preprocessing import LabelEncoder X = df.iloc[:,2:].values y = df.iloc[:,1].values le = LabelEncoder() #将M-B等字符串编码成计算机能识别的0-1 y = le.fit_transform(y) le.transform(['M','B']) # 数据切分8:2 from sklearn.model_selection import train_test_split X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=y,random_state=1) from sklearn.svm import SVC pipe_svc = make_pipeline(StandardScaler(),SVC(random_state=1)) from sklearn.metrics import confusion_matrix pipe_svc.fit(X_train,y_train) y_pred = pipe_svc.predict(X_test) confmat = confusion_matrix(y_true=y_test,y_pred=y_pred) fig,ax = plt.subplots(figsize=(2.5,2.5)) ax.matshow(confmat, cmap=plt.cm.Blues,alpha=0.3) for i in range(confmat.shape[0]): for j in range(confmat.shape[1]): ax.text(x=j,y=i,s=confmat[i,j],va='center',ha='center') plt.xlabel('predicted label') plt.ylabel('true label') plt.show() # 0.9733333333333334
绘制ROC曲线:
# 绘制ROC曲线: from sklearn.metrics import roc_curve,auc from sklearn.metrics import make_scorer,f1_score scorer = make_scorer(f1_score,pos_label=0) gs = GridSearchCV(estimator=pipe_svc,param_grid=param_grid,scoring=scorer,cv=10) y_pred = gs.fit(X_train,y_train).decision_function(X_test) #y_pred = gs.predict(X_test) fpr,tpr,threshold = roc_curve(y_test, y_pred) ###计算真阳率和假阳率 roc_auc = auc(fpr,tpr) ###计算auc的值 plt.figure() lw = 2 plt.figure(figsize=(7,5)) plt.plot(fpr, tpr, color='darkorange', lw=lw, label='ROC curve (area = %0.2f)' % roc_auc) ###假阳率为横坐标,真阳率为纵坐标做曲线 plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--') plt.xlim([-0.05, 1.0]) plt.ylim([-0.05, 1.05]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Receiver operating characteristic ') plt.legend(loc="lower right") plt.show()
第二部分:作业
1.回归和分类的联系和区别,如何用回归问题理解分类问题
回归问题与分类问题需要预测的因变量不一样:
(1)在回归问题中,因变量是连续性变量,我们需要预测
E ( Y ∣ X ) E(Y|X)
E(Y∣X)
是一个连续的实数;
(2)在分类问题中,我们往往是通过已知X的信息预测Y的类别,往往是一个离散集合中的某个元素。如:是否患癌症,图片是猫还是狗等。
2.为啥分类问题的损失函数可以是交叉熵而不是均方误差
更多可以参考:直观理解为什么分类问题用交叉熵损失而不用均方误差损失?
在回归问题中,选择分割点的标准是均方误差,但是在分类问题中,由于因变量是类别变量而不是连续变量,因此用均方误差显然不合适。
决策树:在回归树中,对一个给定的观测值,因变量的预测值取它所属的终端结点内训练集的平均因变量。与之相对应,对于分类树来说,给定一个观测值,因变量的预测值为它所属的终端结点内训练集的最常出现的类。分类树的构造过程与回归树也很类似,与回归树一样,分类树也是采用递归二叉分裂。但是在分类树中,均方误差无法作为确定分裂节点的准则,一个很自然的替代指标是分类错误率。分类错误率就是:此区域内的训练集中非常见类所占的类别,即:
上式中的p ^ m k \hat{p}_{mk}
p
^
mk
代表第m个区域的训练集中第k类所占的比例。但是在大量的事实证明:分类错误率在构建决策树时不够敏感,一般在实际中用如下两个指标代替:
(1) 基尼系数:
在基尼系数的定义中,我们发现这个指标衡量的是K个类别的总方差。不难发现,如果所有的p ^ m k \hat{p}_{mk}
p
^
mk
的取值都接近0或者1,基尼系数会很小。因此基尼系数被视为衡量结点纯度的指标----如果他的取值小,那就意味着某个节点包含的观测值几乎来自同一个类别。
由基尼系数作为指标得到的分类树叫做:CART。
(2) 交叉熵:
可以替代基尼系数的指标是交叉熵,定义如下:
显然,如果所有的p ^ m k \hat{p}_{mk}
p
^
mk
都接近于0或者1,那么交叉熵就会接近0。因此,和基尼系数一样,如果第m个结点的纯度越高,则交叉熵越小。事实证明,基尼系数和交叉熵在数值上时很接近的。
由于线性判别分析的目标是同一类别内方差小,不同类别之间距离大,因此损失函数定义为:
3.线性判别分析和逻辑回归在估计参数方面有啥异同点
线性判别分析不用直接输出概率值确定分类结果;而逻辑回归通过极大似然估计,最后输出概率值。
4.拓展题&面试题:尝试从0推导SVM
看这个的pdf:https://zhuanlan.zhihu.com/p/31652569
5.二次判别分析,线性判别分析,朴素贝叶斯之间的联系和区别
6.必做题:使用python+numpy实现逻辑回归
之前做吴恩达的作业有做到:https://andyguo.blog.csdn.net/article/details/118872719
# coding=utf-8 import numpy as np import matplotlib.pyplot as plt import re import time class RegressionModel(object): """ 逻辑回归模型 """ def __init__(self): self.W = None def train(self, x_train, y_train, learning_rate=0.1, num_iters=10000): """ 模型训练 :param x_train: shape = num_train, dim_feature :param y_train: shape = num_train, 1 :param learning_rate :param num_iters :return: loss_history """ num_train, dim_feature = x_train.shape # w * x + b x_train_ = np.hstack((x_train, np.ones((num_train, 1)))) self.W = 0.001 * np.random.randn(dim_feature + 1, 1) loss_history = [] for i in range(num_iters+1): # linear transformation: w * x + b g = np.dot(x_train_, self.W) # sigmoid: 1 / (1 + e**-x) h = 1 / (1 + np.exp(-g)) # cross entropy: 1/m * sum((y*np.log(h) + (1-y)*np.log((1-h)))) loss = -np.sum(y_train * np.log(h) + (1 - y_train) * np.log(1 - h)) / num_train loss_history.append(loss) # dW = cross entropy' = 1/m * sum(h-y) * x dW = x_train_.T.dot(h - y_train) / num_train # W = W - dW self.W -= learning_rate * dW # debug if i % 100 == 0: print('Iters: %r/%r Loss: %r' % (i, num_iters, loss)) return loss_history def validate(self, x_val, y_val): """ 验证模型效果 :param x_val: shape = num_val, dim_feature :param y_val: shape = num_val, 1 :return: accuracy, metric """ num_val, dim_feature = x_val.shape x_val_ = np.hstack((x_val, np.ones((num_val, 1)))) # linear transformation: w * x + b g = np.dot(x_val_, self.W) # sigmoid: 1 / (1 + e**-x) h = 1 / (1 + np.exp(-g)) # predict y_val_ = h y_val_[y_val_ >= 0.5] = 1 y_val_[y_val_ < 0.5] = 0 true_positive = len(np.where(((y_val_ == 1).astype(int) + (y_val == 1).astype(int) == 2) == True)[0]) * 1.0 / num_val true_negative = len(np.where(((y_val_ == 0).astype(int) + (y_val == 0).astype(int) == 2) == True)[0]) * 1.0 / num_val false_positive = len(np.where(((y_val_ == 1).astype(int) + (y_val == 0).astype(int) == 2) == True)[0]) * 1.0 / num_val false_negative = len(np.where(((y_val_ == 0).astype(int) + (y_val == 1).astype(int) == 2) == True)[0]) * 1.0 / num_val negative_instance = true_negative + false_positive positive_instance = false_negative + true_positive metric = np.array([[true_negative / negative_instance, false_positive / negative_instance], [false_negative / positive_instance, true_positive / positive_instance]]) accuracy = true_positive + true_negative return accuracy, metric def feature_batch_extraction(d_list, kw_set): """ 特征批量提取 :param d_list: 原始数据集 :param kw_set: 关键字列表 :return: """ kw_2_idx_dict = dict(zip(list(kw_set), range(len(kw_set)))) feature_data = np.zeros((len(d_list), len(kw_set))) label_data = np.zeros((len(d_list), 1)) for i in range(len(d_list)): label, words = d_list[i] for word in words: if word in kw_2_idx_dict: feature_data[i, kw_2_idx_dict[word]] = 1 label_data[i] = 1 if label == 'spam' else 0 return feature_data, label_data def data_pre_process(data_file_name): """ 句子切分成单词,由于是英文,所以这里处理方式比较暴力,按照空格和除'之外的符号来切分了;然后全部转小写 :param data_file_name: :return: """ fh = open(data_file_name, encoding='utf-8') data = list() for line in fh.readlines(): label_text_pair = line.split('\t') word_list = re.split('[^\'a-zA-Z]', label_text_pair[1]) word_in_doc_set = set() for raw_word in word_list: word = raw_word.lower() if word == '': continue word_in_doc_set.add(word) # 组成 [[label] [input_text_words]] 的形式 data.append((label_text_pair[0], list(word_in_doc_set))) return data def statistic_key_word(data, cut_off=None): """ 统计单词出现的文档次数,并试图把直观上无效(出现在的文档数目较少)的单词去掉 :param data: data in one line: [label] [input_text] :param cut_off: :return: """ # 针对各个单词,统计单词出现的文档次数 w_dict = dict() total_doc_count = len(data) for _, word_in_doc_set in data: for word in word_in_doc_set: if word not in w_dict: w_dict[word] = 0 w_dict[word] += 1 for word in w_dict.keys(): w_dict[word] /= total_doc_count * 1.0 # 按出现文档次数从高到低,对单词进行排序 w_count_list = sorted(w_dict.items(), key=lambda d: d[1], reverse=True) # 截断后续出现次数过低的单词 kw_set = set() cut_off_length = cut_off if cut_off else len(w_count_list) for word, _ in w_count_list[:cut_off_length]: kw_set.add(word) return w_count_list, kw_set def shuffle(data, k): """ 切分并打乱,为模型的交叉验证做准备 :param data: :param k: :return: """ # 将数据按类别归类,目的是为了切分各个fold的时候,保证数据集合中类别分布平均一些 label_data_dict = dict() for label, word_in_doc_set in data: if label not in label_data_dict: label_data_dict[label] = list() label_data_dict[label].append((label, word_in_doc_set)) # 切分并打乱 k_group_data_list = [list() for _ in range(k)] for label, label_data_list in label_data_dict.items(): # 打乱 seq = np.random.permutation(range(len(label_data_list))) # 切分 fold_instance_count = int(len(label_data_list) / k) for i in range(k): for idx in range(i * fold_instance_count, (i+1) * fold_instance_count): k_group_data_list[i].append(label_data_list[seq[idx]]) k_fold_data_list = list() for i in range(k): train_data = [] for j in range(k): if i != j: train_data.extend(k_group_data_list[j]) k_fold_data_list.append((train_data, k_group_data_list[i])) return k_fold_data_list def draw_loss_list(loss_list): """ 画出单词频次分布情况,为选择一个合适的截断提供直观的依据 :param loss_list: :return: """ plt.figure(figsize=(8, 4)) plt.xlabel('Train iteration') plt.ylabel('Loss') xt_list = range(0, len(loss_list[0][1]), 1000) print(len(loss_list[0][1])) for cut_off, loss in loss_list: print(len(loss)) plt.plot(range(0, len(loss)), loss, label='cut off %r' % (cut_off,)) plt.xticks(xt_list, xt_list) plt.xlim(1, len(loss_list[0][1]) + 1) plt.ylim(0, 0.7) plt.legend() plt.show() def performance_with_cut_off(): """ :return: """ file_name = './data/SMSSpamCollection.txt' raw_data_list = data_pre_process(file_name) fold_count = 4 fold_data_list = shuffle(raw_data_list, fold_count) loss_list = list() accuracy_list = list() metric_list = list() time_cost_list = list() for cut_off in (200, 500, 2000, 5000, 7956): t1 = time.perf_counter() data_list = fold_data_list[0] train_data_list, test_data_list = data_list word_count_list, key_word_set = statistic_key_word(train_data_list, cut_off=cut_off) # Feature extraction train_feature, train_label = feature_batch_extraction(train_data_list, key_word_set) validate_feature, validate_label = feature_batch_extraction(test_data_list, key_word_set) # Train model lr_model = RegressionModel() loss_history = lr_model.train(train_feature, train_label, num_iters=10000) loss_list.append((cut_off, loss_history)) accuracy, metric = lr_model.validate(validate_feature, validate_label) accuracy_list.append(accuracy) metric_list.append(metric) time_cost_list.append((time.perf_counter() - t1)) with open('./result/lr_loss_list.txt', 'w') as f: f.write(str(loss_list) + '\n') f.write(str(accuracy_list) + '\n') f.write(str(time_cost_list) + '\n') f.write(str(metric_list)) with open('./result/lr_loss_list.txt') as f: loss_list = eval(f.readline()) draw_loss_list(loss_list) accuracy_list = eval(f.readline()) print(accuracy_list) time_cost_list = eval(f.readline()) print(time_cost_list) metric_list = eval(f.readline()) print(metric_list) def performance_with_fold(): """ :return: """ file_name = './data/SMSSpamCollection.txt' raw_data_list = data_pre_process(file_name) fold_count = 4 fold_data_list = shuffle(raw_data_list, fold_count) acc_average = 0 cut_off = 500 t1 = time.perf_counter() for fold, data_list in enumerate(fold_data_list): train_data_list, test_data_list = data_list word_count_list, key_word_set = statistic_key_word(train_data_list, cut_off=cut_off) # Feature extraction train_feature, train_label = feature_batch_extraction(train_data_list, key_word_set) validate_feature, validate_label = feature_batch_extraction(test_data_list, key_word_set) # Train model lr_model = RegressionModel() loss_history = lr_model.train(train_feature, train_label) # Validate accuracy, metric = lr_model.validate(validate_feature, validate_label) acc_average += accuracy print('Fold %r/%r - Acc:%r Metric:%r' % (fold + 1, fold_count, accuracy, metric)) print('Average Acc:%r Average Cost Time:%r' % (acc_average / len(fold_data_list), (time.perf_counter() - t1) / len(fold_data_list))) if __name__ == '__main__': performance_with_cut_off()
7.拓展题:了解梯度下降法、牛顿法、拟牛顿法和smo算法等优化算法
常见的几种最优化方法(梯度下降法、牛顿法、拟牛顿法、共轭梯度法等)
SMO算法:https://segmentfault.com/a/1190000014697400
Reference
(1)datawhale萌弟教程
(2)最优化理论之无约束优化基本结构及其python应用:https://zhuanlan.zhihu.com/p/163405865
(3)最优化理论之负梯度方法与Newton型方法:https://zhuanlan.zhihu.com/p/165914126
(4)评估模型的性能并调参:https://zhuanlan.zhihu.com/p/140040705
(5)https://blog.csdn.net/qq_35153620/article/details/95763896
(6)https://www.jianshu.com/p/4cfb4f734358