糖尿病数据集

我们将使用来自糖尿病患者的数据集。 数据由 442 个样本和 10 个变量(都是生理特征)组成,因此它很高而且很窄。 因变量是基线后一年疾病进展的定量测量。

这是一个经典的数据集,由 Efron,Hastie,Johnstone 和 Tibshirani 在他们的最小角度回归的论文中使用,也是 scikit-learn 中包含的众多数据集之一。

  1. data = datasets.load_diabetes()
  2. feature_names=['age', 'sex', 'bmi', 'bp', 's1', 's2', 's3', 's4', 's5', 's6']
  3. trn,test,y_trn,y_test = train_test_split(data.data, data.target, test_size=0.2)
  4. trn.shape, test.shape
  5. # ((353, 10), (89, 10))

Sklearn 中的线性回归

考虑系统Xβ=y,其中X的行比列更多。 当你有比变量更多的数据样本时会发生这种情况。 我们想要找到 七、线性回归和健康结果 - 图1 来最小化:

七、线性回归和健康结果 - 图2

让我们从使用 sklearn 实现开始:

  1. regr = linear_model.LinearRegression()
  2. %timeit regr.fit(trn, y_trn)
  3. # 458 µs ± 62.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
  4. pred = regr.predict(test)

有一些指标来表示我们的预测有多好,会很有帮助。 我们将研究均方范数(L2)和平均绝对误差(L1)。

  1. def regr_metrics(act, pred):
  2. return (math.sqrt(metrics.mean_squared_error(act, pred)),
  3. metrics.mean_absolute_error(act, pred))
  4. regr_metrics(y_test, regr.predict(test))
  5. # (75.36166834955054, 60.629082113104403)

多项式特征

线性回归找到最佳系数βi

七、线性回归和健康结果 - 图3

添加多项式特征仍然是线性回归问题,只需更多项:

七、线性回归和健康结果 - 图4

我们需要使用原始数据X来计算其他多项式特征。

  1. trn.shape
  2. # (353, 10)

现在,我们想通过添加更多功能,来尝试提高模型的表现。 目前,我们的模型在每个变量中都是线性的,但我们可以添加多项式特征来改变它。

  1. poly = PolynomialFeatures(include_bias=False)
  2. trn_feat = poly.fit_transform(trn)
  3. ', '.join(poly.get_feature_names(feature_names))
  4. # 'age, sex, bmi, bp, s1, s2, s3, s4, s5, s6, age^2, age sex, age bmi, age bp, age s1, age s2, age s3, age s4, age s5, age s6, sex^2, sex bmi, sex bp, sex s1, sex s2, sex s3, sex s4, sex s5, sex s6, bmi^2, bmi bp, bmi s1, bmi s2, bmi s3, bmi s4, bmi s5, bmi s6, bp^2, bp s1, bp s2, bp s3, bp s4, bp s5, bp s6, s1^2, s1 s2, s1 s3, s1 s4, s1 s5, s1 s6, s2^2, s2 s3, s2 s4, s2 s5, s2 s6, s3^2, s3 s4, s3 s5, s3 s6, s4^2, s4 s5, s4 s6, s5^2, s5 s6, s6^2'
  5. trn_feat.shape
  6. # (353, 65)
  7. regr.fit(trn_feat, y_trn)
  8. # LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
  9. regr_metrics(y_test, regr.predict(poly.fit_transform(test)))
  10. # (55.747345922929185, 42.836164292252235)

时间对于特征数是平方的,对于样本数是线性的,所以这将变得非常慢!

  1. %timeit poly.fit_transform(trn)
  2. # 635 µs ± 9.25 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

加速特征生成

我们想加快速度。 我们将使用 Numba,一个直接将代码编译为 C 的 Python 库。

Numba 是一个编译器。

资源

Jake VanderPlas 的这个教程是一个很好的介绍。 在这里,Jake 使用 Numba 实现了一个非平凡的算法(非均匀快速傅里叶变换)。

Cython 是另一种选择。 我发现 Cython 主要比 Numba 更多的知识(它更接近 C),但提供类似 Numba 的加速。

七、线性回归和健康结果 - 图5

这里是预先编译(AOT)编译器,即时编译(JIT)编译器和解释器之间差异的全面回答

使用向量化和原生代码进行实验

让我们先了解一下 Numba,然后我们将回到我们的糖尿病数据集回归的多项式特征问题。

  1. %matplotlib inline
  2. import math, numpy as np, matplotlib.pyplot as plt
  3. from pandas_summary import DataFrameSummary
  4. from scipy import ndimage
  5. from numba import jit, vectorize, guvectorize, cuda, float32, void, float64

我们将展示以下方面的影响:

  • 避免内存分配和副本(比 CPU 计算慢)
  • 更好的局部性
  • 向量化

如果我们一次在整个数组上使用 numpy,它会创建大量的临时值,并且不能使用缓存。 如果我们一次使用 numba 循环遍历数组项,那么我们就不必分配大型临时数组,并且可以复用缓存数据,因为我们正在对每个数组项进行多次计算。

  1. # 无类型和没有向量化
  2. def proc_python(xx,yy):
  3. zz = np.zeros(nobs, dtype='float32')
  4. for j in range(nobs):
  5. x, y = xx[j], yy[j]
  6. x = x*2 - ( y * 55 )
  7. y = x + y*2
  8. z = x + y + 99
  9. z = z * ( z - .88 )
  10. zz[j] = z
  11. return zz
  12. nobs = 10000
  13. x = np.random.randn(nobs).astype('float32')
  14. y = np.random.randn(nobs).astype('float32')
  15. %timeit proc_python(x,y)
  16. # 49.8 ms ± 1.19 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

NumPy

Numpy 让我们对其向量化:

  1. # 有类型和向量化
  2. def proc_numpy(x,y):
  3. z = np.zeros(nobs, dtype='float32')
  4. x = x*2 - ( y * 55 )
  5. y = x + y*2
  6. z = x + y + 99
  7. z = z * ( z - .88 )
  8. return z
  9. np.allclose( proc_numpy(x,y), proc_python(x,y), atol=1e-4 )
  10. # True
  11. %timeit proc_numpy(x,y) # Typed and vectorized
  12. # 35.9 µs ± 166 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Numba

Numba 提供几种不同的装饰器。 我们将尝试两种不同的方法:

  • @jit:非常一般
  • @vectorize:不需要编写for循环。操作相同大小的向量时很有用

首先,我们将使用 Numba 的jit(即时)编译器装饰器,而无需显式向量化。 这避免了大量内存分配,因此我们有更好的局部性:

  1. @jit()
  2. def proc_numba(xx,yy,zz):
  3. for j in range(nobs):
  4. x, y = xx[j], yy[j]
  5. x = x*2 - ( y * 55 )
  6. y = x + y*2
  7. z = x + y + 99
  8. z = z * ( z - .88 )
  9. zz[j] = z
  10. return zz
  11. z = np.zeros(nobs).astype('float32')
  12. np.allclose( proc_numpy(x,y), proc_numba(x,y,z), atol=1e-4 )
  13. # True
  14. %timeit proc_numba(x,y,z)
  15. # 6.4 µs ± 17.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

现在我们将使用 Numba 的vectorize装饰器。 Numba 的编译器以比普通 Python 和 Numpy 更聪明的方式优化它。 它为你写了一个 Numpy ufunc,传统上它涉及编写 C 并且不那么简单。

  1. @vectorize
  2. def vec_numba(x,y):
  3. x = x*2 - ( y * 55 )
  4. y = x + y*2
  5. z = x + y + 99
  6. return z * ( z - .88 )
  7. np.allclose(vec_numba(x,y), proc_numba(x,y,z), atol=1e-4 )
  8. # True
  9. %timeit vec_numba(x,y)
  10. # 5.82 µs ± 14.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Numba 很棒。 看看这有多快!

Numba 多项式特征

  1. @jit(nopython=True)
  2. def vec_poly(x, res):
  3. m,n=x.shape
  4. feat_idx=0
  5. for i in range(n):
  6. v1=x[:,i]
  7. for k in range(m): res[k,feat_idx] = v1[k]
  8. feat_idx+=1
  9. for j in range(i,n):
  10. for k in range(m): res[k,feat_idx] = v1[k]*x[k,j]
  11. feat_idx+=1

行序和列序存储

来自 Eli Bendersky 的博客文章

“矩阵的行序布局将第一行放在连续的内存中,然后是第二行放在它后面,然后是第三行,依此类推。列序布局将第一列放在连续内存中,然后放入第二列,等等….虽然知道特定数据集使用哪种布局对于良好的性能至关重要,但对于哪种布局“更好”的问题,没有单一的答案。”

“事实证明,匹配算法与数据布局的工作方式,可以决定应用程序的性能。”

“简短的说法是:始终按照布局顺序遍历数据。”

列序布局:Fortran,Matlab,R 和 Julia

行序布局:C,C ++,Python,Pascal,Mathematica

  1. trn = np.asfortranarray(trn)
  2. test = np.asfortranarray(test)
  3. m,n=trn.shape
  4. n_feat = n*(n+1)//2 + n
  5. trn_feat = np.zeros((m,n_feat), order='F')
  6. test_feat = np.zeros((len(y_test), n_feat), order='F')
  7. vec_poly(trn, trn_feat)
  8. vec_poly(test, test_feat)
  9. regr.fit(trn_feat, y_trn)
  10. # LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
  11. regr_metrics(y_test, regr.predict(test_feat))
  12. # (55.74734592292935, 42.836164292252306)
  13. %timeit vec_poly(trn, trn_feat)
  14. # 7.33 µs ± 19.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

回想一下,这是 sklearn PolynomialFeatures实现的时间,它是由专家创建的:

  1. %timeit poly.fit_transform(trn)
  2. # 635 µs ± 9.25 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
  3. 605/7.7
  4. # 78.57142857142857

这是一个大问题! Numba 太神奇了! 只需一行代码,我们就可以获得比 scikit 学习快 78 倍的速度(由专家优化)。

正则化和噪声

正则化是一种减少过拟合,并创建更好地泛化到新数据的模型的方法。

正则化

Lasso 回归使用 L1 惩罚,产生稀疏系数。 参数α用于加权惩罚项。 Scikit Learn 的LassoCV使用许多不同的α值进行交叉验证。

观看 Lasso 回归的 Coursera 视频,了解更多信息。

  1. reg_regr = linear_model.LassoCV(n_alphas=10)
  2. reg_regr.fit(trn_feat, y_trn)
  3. '''
  4. /home/jhoward/anaconda3/lib/python3.6/site-packages/sklearn/linear_model/coordinate_descent.py:484: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Fitting data with very small alpha may cause precision problems.
  5. ConvergenceWarning)
  6. LassoCV(alphas=None, copy_X=True, cv=None, eps=0.001, fit_intercept=True,
  7. max_iter=1000, n_alphas=10, n_jobs=1, normalize=False, positive=False,
  8. precompute='auto', random_state=None, selection='cyclic', tol=0.0001,
  9. verbose=False)
  10. '''
  11. reg_regr.alpha_
  12. # 0.0098199431661591518
  13. regr_metrics(y_test, reg_regr.predict(test_feat))
  14. # (50.0982471642817, 40.065199085003101)

噪声

现在我们将为数据添加一些噪音。

  1. idxs = np.random.randint(0, len(trn), 10)
  2. y_trn2 = np.copy(y_trn)
  3. y_trn2[idxs] *= 10 # label noise
  4. regr = linear_model.LinearRegression()
  5. regr.fit(trn, y_trn)
  6. regr_metrics(y_test, regr.predict(test))
  7. # (51.1766253181518, 41.415992803872754)
  8. regr.fit(trn, y_trn2)
  9. regr_metrics(y_test, regr.predict(test))
  10. # (62.66110319520415, 53.21914420254862)

Huber 损失是一种损失函数,对异常值的敏感度低于平方误差损失。 对于小的误差值,它是二次的,对于大的值,它是线性的。

七、线性回归和健康结果 - 图6

  1. hregr = linear_model.HuberRegressor()
  2. hregr.fit(trn, y_trn2)
  3. regr_metrics(y_test, hregr.predict(test))
  4. # (51.24055602541746, 41.670840571376822)