第六节:模型选择

模型选择

  机器学习是在某种假设上对数据的分析,基于该假设即可构造多个模型获得预测值,通过比较多个模型间真实值与预测值之间的误差即可获得一个较优的模型。

  由于机器学习不是预言而是预测。因此机器学习可能会出现欠拟合和过拟合的现象,即如果模型拟合效果不好,则是欠拟合,对于欠拟合问题通常增大训练数据量即可;但是如果模型的拟合效果很好,也并不一定是一件好事,因为训练集中往往含有噪声,导致模型可能出现过拟合,过拟合将会是学习机器学习路上的拦路虎之一。

  在未来很长的一段路中首先要考虑的就是模型会不会过拟合,因此本文给出了解决过拟合的几种方法,解决过拟合问题的同时就是在进行模型选择。

  除了过拟合问题之外,模型可能还会因为其他的问题造成模型不优的情况,通常回归问题可以通过偏差和方差度量模型性能,二分类问题则可以通过精准度和查全率的考虑或描绘学习曲线ROC来度量模型性能。

  总而言之,总能通过某种工具的应用让我们找到一个最优的模型来预测未来新数据。

模型选择学习目标

  1. 损失函数和目标函数
  2. 过拟合
  3. 解决过拟合的四种方法
  4. 偏差与方差
  5. 查准率、查全率和F1
  6. ROC和AUC

机器学习基本假设

损失函数

  机器学习模型通常分为概率模型和非概率模型,概率模型由条件概率分布$p(\hat{y_i}|x_i)$表示;非概率模型由决策函数$\hat{y_i}=f(x_i)$表示,其中$\hat{y_i}$为模型的输出值即预测值。

  通常情况下使用损失函数度量模型对单个样本预测的好坏,即度量真实值与预测值之间的误差,一般使用下面给出的$4$种损失函数。

  给定一个样例$(x_i,y_i)$,假设模型的预测值为$\hat{y_i}$,把损失函数记作$L(yi,\hat{y_i})$.通常有以下4种损失函数的表现形式。

  1. 0-1损失函数:
    $$
    L(y_i,\hat{y_i})= \begin{cases} 1, &{y_i\not=\hat{y_i}} \ 0, &{y_i=\hat{y_i}} \end{cases}
    $$
  2. 平方损失函数:
    $$
    L(y_i,\hat{y_i})=(y_i-\hat{y_i})^2
    $$
  3. 绝对值损失函数:
    $$
    L(y_i,\hat{y_i})=|y_i-\hat{y_i}|
    $$
  4. 对数损失函数或对数似然损失函数:
    $$
    L(y_i,p(\hat{y_i}|x_i))=-logp(\hat{y_i}|x_i)
    $$

目标函数

  通过损失函数即可得到模型的目标函数,目标函数是数据集的平均损失,它表示训练集的平均训练误差,有时候也被称为训练误差,对于测试集则称为测试误差。

  有一个$n$个鸢尾花样本的训练集$T={(x_1,y_1),(x_2,y_2),\cdots,(x_i,y_i),\cdots,(x_n,yn)}$,可以给出目标函数的公式:
$$
J={\frac{1}{n}}\sum
{i=1}^nL(y_i,\hat{y_i})
$$
  当损失函数是0-1损失函数时,目标函数将变成误差率或准确率,$I(y_i \not= \hat{yi})$是指示函数,即$y_i \not= \hat{yi}$时为1,否则为0。
$$
Je={\frac{1}{n}} \sum{i=1}^n I(y_i\not=\hat{y_i}) \text{误差率}\
Ja={\frac{1}{n}} \sum{i=1}^n I(y_i = \hat(y_i)) \text{准确率}\
J_e + J_a = 1
$$

参数模型和非参数模型

参数模型

  参数模型是通过训练大量的训练集得到一个的带有参数的函数,预测未来新数据时只需要把特征值输入函数即可获得预测值。参数模型的典型例子有感知机、逻辑回归。

非参数模型

  非参数模型无法用一组固定的参数来描述,通常情况下利用数据本身进行训练。非参数模型的典型例子有决策树、k-近邻算法。

过拟合

  上一节讲了通过训练误差选择最优模型,也许已经找到了$0$误差的模型,但是这就是最好的吗?

  事实上$0$误差的模型也许并不是最好的,因为模型是通过训练集得到的,由于训练集可能存在噪声,因此训练集并不一定能代表测试集,更不一定能代表未来新数据。虽然这样的模型可能很好的拟合训练数据,但是对未来数据可能并没有较好的拟合能力,这种现象成为过拟合。

过拟合解决方法

  1. 收集更多训练数据
  2. 选择参数较少的简单模型
  3. 通过正则化引入对复杂性的惩罚
  4. 切割训练数据集交叉验证
  5. 减少数据的维度或者删除掉无意义的特征(会有单独的文章介绍数据降维)

收集更多训练数据

  由于还没有实战经验,你可能无法想象。

  但是你可以这样假想当训练数据多的能够穷尽未来新数据的时候,那么用该训练数据得到的模型去测试未来新数据,那么对未来新数据的预测一定能够得到一个很好的预测值;训练数据过多也会适当减轻噪声的影响。

选择简单模型

  给定一个训练集$T={(x_1,y_1),(x_2,y_2),\cdots,(x_i,y_i),\cdots,(x_n,y_n)}$,第$i$个样本$x_i$可以由一个$m$次多项式函数得到一个预测值$\hat{y_i}$。假设$m$次多项式为
$$
\hat{y_i}=f(x,\omega)=\omega_1x_i^{(1)}+\omega_2x_i^{(2)}+\cdots+\omega_nx_i^{(n)}
$$

示例

# 过拟合图例
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
font = FontProperties(fname='/Library/Fonts/Heiti.ttc')
%matplotlib inline

# 自定义数据并处理数据
data_frame = {'x': [2, 1.5, 3, 3.2, 4.22, 5.2, 6, 6.7],
              'y': [0.5, 3.5, 5.5, 5.2, 5.5, 5.7, 5.5, 6.25]}
df = pd.DataFrame(data_frame)
X, y = df.iloc[:, 0].values.reshape(-1, 1), df.iloc[:, 1].values.reshape(-1, 1)

# 线性回归
lr = LinearRegression()
lr.fit(X, y)

def poly_lr(degree):
    """多项式回归"""
    poly = PolynomialFeatures(degree=degree)
    X_poly = poly.fit_transform(X)
    lr_poly = LinearRegression()
    lr_poly.fit(X_poly, y)
    y_pred_poly = lr_poly.predict(X_poly)

    return y_pred_poly

def plot_lr():
    """对线性回归生成的图线画图"""
    plt.scatter(X, y, c='k', edgecolors='white', s=50)
    plt.plot(X, lr.predict(X), color='r', label='lr')
    # 噪声
    plt.scatter(2, 0.5, c='r')
    plt.text(2, 0.5, s='$(2,0.5)$')

    plt.xlim(0, 7)
    plt.ylim(0, 8)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.legend()

def plot_poly(degree, color):
    """对多项式回归生成的图线画图"""
    plt.scatter(X, y, c='k', edgecolors='white', s=50)
    plt.plot(X, poly_lr(degree), color=color, label='m={}'.format(degree))
    # 噪声
    plt.scatter(2, 0.5, c='r')
    plt.text(2, 0.5, s='$(2,0.5)$')

    plt.xlim(0, 7)
    plt.ylim(0, 8)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.legend()

def run():
    plt.figure()
    plt.subplot(231)
    plt.title('图1(线性回归)', fontproperties=font, color='r', fontsize=12)
    plot_lr()
    plt.subplot(232)
    plt.title('图2(一阶多项式回归)', fontproperties=font, color='r', fontsize=12)
    plot_poly(1, 'orange')
    plt.subplot(233)
    plt.title('图3(三阶多项式回归)', fontproperties=font, color='r', fontsize=12)
    plot_poly(3, 'gold')
    plt.subplot(234)
    plt.title('图4(五阶多项式回归)', fontproperties=font, color='r', fontsize=12)
    plot_poly(5, 'green')
    plt.subplot(235)
    plt.title('图5(七阶多项式回归)', fontproperties=font, color='r', fontsize=12)
    plot_poly(7, 'blue')
    plt.subplot(236)
    plt.title('图6(十阶多项式回归)', fontproperties=font, color='r', fontsize=12)
    plot_poly(10, 'violet')
    plt.show()

run()

  如上图所示每张图都有相同分布的8个样本点,红点明显是一个噪声点,接下来将讲解上述8张图。暂时不用太关心线性回归和多项式回归是什么,这两个以后你都会学习到,此处引用只是为了方便举例。

  • 图1:线性回归拟合样本点,可以发现样本点距离拟合曲线很远,这个时候一般称作欠拟合(underfitting)
  • 图2:一阶多项式回归拟合样本点,等同于线性回归
  • 图3:三阶多项式回归拟合样本点,表现还不错
  • 图4:五阶多项式回归拟合样本点,明显过拟合
  • 图5:七阶多项式回归拟合样本点,已经拟合了所有的样本点,毋庸置疑的过拟合
  • 图7:十阶多项式回归拟合样本点,拟合样本点的曲线和七阶多项式已经没有了区别,可以想象十阶之后的曲线也类似于七阶多项式的拟合曲线

  从上图可以看出,过拟合模型将会变得复杂,对于线性回归而言,它可能需要更高阶的多项式去拟合样本点,对于其他机器学习算法,也是如此。这个时候你也可以想象,过拟合虽然对拟合的样本点的误差接近0,但是对于未来新数据而言,如果新数据的$x=2$,如果使用过拟合的曲线进行拟合新数据,那么会给出$y=0.5$的预测值,也就是说把噪声的值给了新数据,这样明显是不合理的。

正则化

  针对过拟合有时候可以减少模型的参数,还有一个典型的方法是对目标函数正则化(regularization),即在目标函数上加上一个正则化项(regularizer)或惩罚项(penalty term),即新的目标函数变成
$$
J(\omega)=\frac{1}{m} \sum_{i=1}^m L(yi,f{\omega_i}(x_i)) + \lambda(R(f))
$$
其中$\lambda\geq0$为超参数,类似于参数,但是参数可以通过算法求解,超参数需要人工手动调整;$\lambda(R(f))$为正则化项。

  正则化项一般是一个单调递增的函数,模型越复杂,正则化值越大,惩罚越大。

L1正则化

  L1正则化(Lasso)是在目标函数上加上L1正则化项,一般用于特征选择,也可以防止过拟合,即新的目标函数为
$$
J(\omega) =\frac{1}{m} \sum_{i=1}^m L(yi,f{\omega_i}(x_i)) + \lambda||\omega||_1
$$
其中$||\omega||_1$为参数向量$\omega$的1范数。

  假设样本有$n$特征,则$\omega$为$n$维向量,1范数为
$$
||\omega||1=\sum{j=1}^n|\omega_j|
$$

L2正则化

  L2正则化(Ridge)是在目标函数上加上L2正则化项,一般只用于过拟合,即新的目标函数为
$$
J(\omega)=\frac{1}{m} \sum_{i=1}^m L(yi,f{\omega_i}(x_i)) + \frac{\lambda}{2}||\omega||_2^2
$$
其中$||\omega||_2^2$为参数向量$\omega$的2范数的平方。

  假设样本有$n$特征,则$\omega$为$n$维向量,2范数为
$$
||\omega||2=\sqrt{\sum{j=1}^n{\omega_j}^2}
$$

  多说一嘴,假设样本有$n$特征,则$\omega$为$n$维向量,p范数为
$$
||\omega||p=\sqrt[p]{\sum{j=1}^n{\omega_j}^p}
$$

交叉验证

  对训练数据集切割做交叉验证也是防止模型过拟合的一个很好的方法。

  一般会把数据按照某种比例分为训练集、测试集。训练集用来训练模型,把测试集当做未来新样本的样本集用来评估模型。然后交叉验证可以认为就是不断地重复训练模型、测试模型。

简单交叉验证

  把数据集按照某种比例,将数据集中的数据随机的分为训练集和测试集。然后不断的改变模型参数训练出一组模型,每训练完一个模型就用测试集测试,最后得到性能最好的模型。

  1. 初始值$c=1$
  2. 训练模型
  3. 测试模型,$c+1$
  4. 如果$c<11$改变模型参数,跳转到步骤1;反之,停止训练
  5. 从模型集${c_1,c2,\cdots,c{10}}$中选择性能最优的模型
# 简单交叉验证
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split

# 导入鸢尾花数据
iris_data = datasets.load_iris()
X = iris_data.data[:, [0, 1]]
y = iris_data.target

# random_state=1可以确保结果不随机,stratify=y可以确保每个分类的结果都有相同的比例
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=1, stratify=y)

print('不同类别所有样本数量:{}'.format(np.bincount(y)))
print('不同类别训练数据数量:{}'.format(np.bincount(y_train)))
print('不同类别测试数据数量:{}'.format(np.bincount(y_test)))
不同类别所有样本数量:[50 50 50]
不同类别训练数据数量:[35 35 35]
不同类别测试数据数量:[15 15 15]

k折交叉验证

  将数据随机的分为$k$个子集($k$的取值范围一般在$[1-20]$之间),然后取出$k-1$个子集进行训练,另一个子集用作测试模型,重复$k$次这个过程,得到最优模型。

  1. 将数据分为$k$个子集
  2. 选择$k-1$个子集训练模型
  3. 选择另一个子集测试模型
  4. 重复2-3步,直至有$k$个模型
  5. 选择$k$个模型中性能最优的模型
# k折交叉验证
import numpy as np
from sklearn import datasets
from sklearn.model_selection import StratifiedKFold

# 导入鸢尾花数据
iris_data = datasets.load_iris()
X = iris_data.data[:, [0, 1]]
y = iris_data.target

# n_splits=10相当于k=10
kfold = StratifiedKFold(n_splits=10, random_state=1)
kfold = kfold.split(X, y)

for k, (train_data, test_data) in enumerate(kfold):
    print('迭代次数:{}'.format(k), '训练数据长度:{}'.format(
        len(train_data)), '测试数据长度:{}'.format(len(test_data)))
迭代次数:0 训练数据长度:135 测试数据长度:15
迭代次数:1 训练数据长度:135 测试数据长度:15
迭代次数:2 训练数据长度:135 测试数据长度:15
迭代次数:3 训练数据长度:135 测试数据长度:15
迭代次数:4 训练数据长度:135 测试数据长度:15
迭代次数:5 训练数据长度:135 测试数据长度:15
迭代次数:6 训练数据长度:135 测试数据长度:15
迭代次数:7 训练数据长度:135 测试数据长度:15
迭代次数:8 训练数据长度:135 测试数据长度:15
迭代次数:9 训练数据长度:135 测试数据长度:15

留一法交叉验证

  与$k$折交叉验证类似,属于$k$折交叉验证的特例,即一个数据集$T$中有$n$个数据,当$k=n-1$时,$k$折交叉验证即为留一法交叉验证。

# 留一法交叉验证
import numpy as np
from sklearn import datasets
from sklearn.model_selection import LeaveOneOut

# 导入鸢尾花数据
iris_data = datasets.load_iris()
X = iris_data.data[:, [0, 1]]
y = iris_data.target

loo = LeaveOneOut()
loo
LeaveOneOut()
loo.get_n_splits(X)
150
count = 0
for train_index, test_index in loo.split(X):
    if count < 10:
        print("训练集长度:", len(train_index), "测试集长度:", len(test_index))
    count += 1
    if count == loo.get_n_splits(X)-1:
        print('...\n迭代次数:', count)
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
训练集长度: 149 测试集长度: 1
...
迭代次数: 149

时间序列分割

  时间序列分割一般对时间序列算法做测试,他切割的原理是:测试集的数据和上几个数据会有一定的联系。

from sklearn.model_selection import TimeSeriesSplit
X = np.array([[1, 2], [2, 4], [3, 2], [2, 4], [1, 2], [3, 2]])
y = np.array([1, 3, 3, 4, 5, 4])
# max_train_size指训练数据个数,n_splits指切割次数
tscv = TimeSeriesSplit(n_splits=5, max_train_size=3)
tscv
TimeSeriesSplit(max_train_size=3, n_splits=5)
for train_index, test_index in tscv.split(X):
    print("训练数据索引:", train_index, "测试数索引:", test_index)
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
训练数据索引: [0] 测试数索引: [1]
训练数据索引: [0 1] 测试数索引: [2]
训练数据索引: [0 1 2] 测试数索引: [3]
训练数据索引: [1 2 3] 测试数索引: [4]
训练数据索引: [2 3 4] 测试数索引: [5]

交叉验证和模型一起使用

  如果只是对交叉验证有一定的了解,那么问题则是,我们如何把使用交叉验证的思想,训练模型呢?使用for循环吗?不,我们可以使用sklearn自带的交叉验证评分方法。

cross_val_score

  交叉验证中的cross_val_score,即最普通的交叉验证和模型一起使用的方法,该方法需要指定模型、训练集数据和评分方法,然后可以得出每一次测试模型的分数。

from sklearn.metrics import SCORERS

# 可以使用的评分方法
SCORERS.keys()
dict_keys(['explained_variance', 'r2', 'neg_median_absolute_error', 'neg_mean_absolute_error', 'neg_mean_squared_error', 'neg_mean_squared_log_error', 'accuracy', 'roc_auc', 'balanced_accuracy', 'average_precision', 'neg_log_loss', 'brier_score_loss', 'adjusted_rand_score', 'homogeneity_score', 'completeness_score', 'v_measure_score', 'mutual_info_score', 'adjusted_mutual_info_score', 'normalized_mutual_info_score', 'fowlkes_mallows_score', 'precision', 'precision_macro', 'precision_micro', 'precision_samples', 'precision_weighted', 'recall', 'recall_macro', 'recall_micro', 'recall_samples', 'recall_weighted', 'f1', 'f1_macro', 'f1_micro', 'f1_samples', 'f1_weighted'])
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn import datasets

iris = datasets.load_iris()
X = iris.data
y = iris.target

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000)
scores = cross_val_score(clf, X, y, cv=10, scoring='accuracy')
scores
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=1000, multi_class='auto',
          n_jobs=None, penalty='l2', random_state=None, solver='lbfgs',
          tol=0.0001, verbose=0, warm_start=False)
print('准确率:{:.4f}(+/-{:.4f})'.format(scores.mean(), scores.std()*2))

cross_validate

  交叉验证中cross_validate方法,相比较cross_val_score方法可以指定多个指标,并且cross_validate方法会返回模型fit_time训练和score_time评分的时间。

from sklearn.model_selection import cross_validate
from sklearn.linear_model import LogisticRegression
from sklearn import datasets

iris = datasets.load_iris()
X = iris.data
y = iris.target

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000)
cross_validate(clf, X, y, cv=10, scoring=[
    'accuracy', 'recall_weighted'], return_train_score=True)
{'fit_time': array([0.03741813, 0.03649879, 0.0418241 , 0.03404689, 0.02642822,
        0.02773309, 0.02451205, 0.02093697, 0.03865075, 0.06034207]),
 'score_time': array([0.00159192, 0.00155306, 0.00085807, 0.00101495, 0.00089979,
        0.000772  , 0.00068307, 0.00121713, 0.00101519, 0.00123286]),
 'test_accuracy': array([1.        , 0.93333333, 1.        , 1.        , 0.93333333,
        0.93333333, 0.93333333, 1.        , 1.        , 1.        ]),
 'train_accuracy': array([0.97037037, 0.97777778, 0.97037037, 0.97037037, 0.97777778,
        0.97777778, 0.98518519, 0.97037037, 0.97037037, 0.97777778]),
 'test_recall_weighted': array([1.        , 0.93333333, 1.        , 1.        , 0.93333333,
        0.93333333, 0.93333333, 1.        , 1.        , 1.        ]),
 'train_recall_weighted': array([0.97037037, 0.97777778, 0.97037037, 0.97037037, 0.97777778,
        0.97777778, 0.98518519, 0.97037037, 0.97037037, 0.97777778])}

cross_val_predict

  交叉验证中的cross_val_predict方法可以获取每个样本的预测结果,即每一个样本都会被作为测试数据。

from sklearn.model_selection import cross_val_predict
from sklearn.linear_model import LogisticRegression
from sklearn import datasets

iris = datasets.load_iris()
X = iris.data
y = iris.target

clf = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=1000)
cross_val_predict(clf, X, y, cv=10)
array([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, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 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])
from sklearn.metrics import accuracy_score

accuracy_score(y, per_sample)
0.9733333333333334

偏差与方差

  通常把模型对未来新数据的预测能力称为泛化能力,而模型对位置新数据预测的误差称为泛化误差。偏差则表达了模型的期望预测与真实结果的偏离程度,即模型的拟合能力;方差表达了同样大小的训练集变动所导致的学习性能的变化,即数据扰动造成的影响;噪声表达了期望泛化误差的下限,即对未来预测新数据的难度。

  回归任务中泛化误差通过某种计算可以分解为偏差、方差与噪声之和,如何分解不在本文讨论范围内。噪声不可避免,因此主要关注偏差与方差,偏差越大,预测值与真实结果的偏离程度越大,模型的拟合能力越弱,泛化误差越大;方差越大,对数据扰动造成的影响越强,模型的拟合能力越差,泛化误差越大。如此看来,偏差与方差越小越好,但是偏差和方差通常情况下是有冲突的,称为偏差-方差窘境。

偏差-方差窘境

  偏差-方差窘境:训练初期,由于训练不足,会造成欠拟合,数据拟合效果很差,偏差较大,数据集的扰动也无法使模型造成较大影响;随着训练程度的加深,数据拟合效果很好,即过拟合,偏差变小,但是数据集轻微的扰动都会使模型发生较大的影响,方差偏大。简而言之,欠拟合状态,偏差大,方差小;过拟合状态,偏差小,方差大。

查准率、查全率和F1

  二分类问题中根据样例的真实类别和模型预测类别的组合划分为真正例(true positive)、假正例(false positive)、真反例(true negative)、假反例(false negative)四种情形,令TP、FP、TN、FN分别表示对应的样例数,$样例总数 = TP+FP+TN+FN$。

    TP——将正类预测为正类数

    FP——将负类预测为正类数

    TN——将负类预测为负类数

    FN——将正类预测为负类数

准确度

准确度(accuracy_socre)定义为
$$
P = {\frac{TP+FN}{TP+FP+TN+FN}} = \frac{正确预测的样本数}{样本总数}
$$

# 查准率示例
from sklearn import datasets
from sklearn.metrics import accuracy_score
from sklearn.linear_model import LogisticRegression

iris_data = datasets.load_iris()
X = iris_data.data
y = iris_data.target

lr = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=200)
lr = lr.fit(X, y)

y_pred = lr.predict(X)
print('准确度:{:.2f}'.format(
    accuracy_score(y, y_pred)))
准确度:0.97

查准率

  查准率定义为
$$
P = {\frac{TP}{TP+FP}} = \frac{正确预测为正类的样本数}{预测为正类的样本总数}
$$

# 查准率示例
from sklearn import datasets
from sklearn.metrics import precision_score
from sklearn.linear_model import LogisticRegression

iris_data = datasets.load_iris()
X = iris_data.data
y = iris_data.target

lr = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=200)
lr = lr.fit(X, y)

y_pred = lr.predict(X)
print('查准率:{:.2f}'.format(
    precision_score(y, y_pred, average='weighted')))
查准率:0.97

查全率

  查全率定义为
$$
R = {\frac{TP}{TP+FN}} = \frac{正确预测为正类的样本数}{正类总样本数}
$$

# 查全率示例
from sklearn.metrics import recall_score
from sklearn import datasets
from sklearn.linear_model import LogisticRegression

iris_data = datasets.load_iris()
X = iris_data.data
y = iris_data.target

lr = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=200)
lr = lr.fit(X, y)

y_pred = lr.predict(X)
print('查全率:{:.2f}'.format(recall_score(y, y_pred, average='weighted')))
查全率:0.97

F1值

  通常情况下通过查准率和查全率度量模型的好坏,但是查准率和查全率是一对矛盾的度量工具,查准率高的时候查全率低;查全率高的时候查准率低,因此工业上对不不同的问题对查准率和查全率的侧重点会有所不同。

  例如癌症的预测中,正类是健康,反类是患有癌症。较高的查准率可能会导致健康的人被告知患有癌症;较高的查全率可能会导致患有癌症的患者会被告知健康。

  $F_1$值定义为
$$
F1 = {\frac{2PR}{P+R}} = {\frac{2TP}{2TP+FP+FN}} = {\frac{2TP}{样例总数+TP-TN}}
$$
  $F
\beta$定义为:
$$
F\beta = {\frac{(1+\beta^2)PR}{\beta^2*P+R}}
$$
  $F
\beta$是在$F_1$值的基础上加权得到的,它可以更好的权衡查准率和查全率。

  1. 当$\beta<1$时,$P$的权重减小,即$R$查准率更重要
  2. 当$\beta=1$时,$F_\beta = F_1$
  3. 当$\beta>1$时,$P$的权重增大,即$P$查全率更重要。
# F1值示例
from sklearn import datasets
from sklearn.metrics import f1_score
from sklearn.linear_model import LogisticRegression

iris_data = datasets.load_iris()
X = iris_data.data
y = iris_data.target

lr = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=200)
lr = lr.fit(X, y)

y_pred = lr.predict(X)
print('F1值:{:.2f}'.format(f1_score(y, y_pred, average='weighted')))
F1值:0.97

ROC曲线

  下图即ROC曲线的图形,对于以下三条ROC曲线,一般情况可以看过$(0,0)$的虚线与ROC曲线的交点,如果交点越靠外面,则模型性能越好。但是一般情况是通过判断ROC曲线围成的面积AUC面积来判断模型的性能。

  通常情况下也会使用ROC(receiver operating characteristic,ROC)曲线度量模型性能的好坏,ROC曲线顾名思义是一条曲线,它的横轴是假正例率(false positive rate,FPR),纵轴是真正例率(true positive rate,TPR),假正例率和真正例率分别定义为:
$$
FPR = {\frac{FP}{TN+FP}} \text{假正例率} \
TPR = {\frac{TP}{TP+FN}} \text{真正例率}
$$

# ROC示例
from sklearn import datasets
from sklearn.metrics import roc_curve
from sklearn.linear_model import LogisticRegression

iris_data = datasets.load_iris()
X = iris_data.data[0:100, :]
y = iris_data.target[0:100]

lr = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=200)
lr = lr.fit(X, y)

y_pred = lr.predict(X)
fpr, tpr, thresholds = roc_curve(y, y_pred)
plt.xlabel('FPR', fontsize=15)
plt.ylabel('TPR', fontsize=15)
plt.title('FPR-TPR', fontsize=20)
plt.plot(fpr, tpr) 
plt.show()

AUC面积

  由于ROC曲线有时候无法精准度量模型的好坏,因此会使用ROC曲线关于横纵轴围成的面积称为AUC(area under ROC curve,AUC)来度量模型的好坏,AUC值越大的模型,则模型越优。

# AUC示例
from sklearn import datasets
from sklearn.metrics import roc_auc_score
from sklearn.linear_model import LogisticRegression

iris_data = datasets.load_iris()
X = iris_data.data[0:100, :]
y = iris_data.target[0:100]

lr = LogisticRegression(solver='lbfgs', multi_class='auto', max_iter=200)
lr = lr.fit(X, y)

y_pred = lr.predict(X)
# 计算AUC值
print('AUC值:{:.2f}'.format(roc_auc_score(y, y_pred, average='weighted')))
AUC值:1.00

小结

  在训练模型的时候,总会有各种各样的原因导致我们的模型可能有一些较大的误差。又由于误差是无法避免的,只能减小,因此我们需要懂得如何去面对并解决这些误差,而不能让误差导致我们在构造模型之前的努力功亏一篑。

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术