两天的时间,我们能用来做什么?

时光总嘲笑我的痴心妄想 提交于 2020-08-11 12:32:37

(这里是本章会用到的 GitHub 地址,我给它起名为了 carefree-ml,观众老爷们可以赏一个 star 和 fork 么 ( σ'ω')σ)

carefree-mlgithub.com

观众老爷们大家好!最近实在太忙,回首一看上篇专栏文章已经是两年半前的事了(?!),所以今天想着写出一篇来撑撑场子(喂

但感觉已经没有当初写专栏的感觉了,所以可能画风会变不少,观众老爷们还望不要介意(逃

(说到底上面这两段就是原封不动地把上篇的前两段粘过来的这样真的好吗

这次想和大家分享的是我最近面试实验室实习生面试到头晕目眩时突发奇想(不)、并用两天时间实现的一个 repo。实现这个 repo 恰好也算是达成了我一直以来未竟的两个心愿:

  • 探索机器学习算法到底可以简化成什么样
  • 探索各种机器学习算法间的共性究竟有多少

现在其实人工智能的热度也渐渐下来了,所以这篇文章更像是为还对一些小细节感兴趣的观众老爷们准备的惊喜礼物吧。如果你恰好也有上述这两个疑惑,又或是想教导其他人这方面的直观,那么 carefree-ml 可能就会比较适合你。但是,如果你对机器学习有着更高的追求,对各种美妙的性质有着探索的欲望,那么 carefree-ml 反而可能会激怒你,因为它省略了很多前人研究出来的结晶

注:这篇文章中可能会冒出些大家不太熟悉的名词;我会对之前写过相应文章的给个相应的链接,而如果我没给相应链接且有观众老爷想了解细节的话,欢迎在评论区留言,我后续会看时间(与相应细节的受欢迎程度)(说白了就是赞的数目)来写一些相应的文章 ( σ'ω')σ

carefree-ml 实现了哪些算法?

  • 1 维的多项式拟合(其实就是包装了一下 np.polyfit 这个函数)
  • 各种线性模型(Linear Regression, Logistic Regression, Linear SVC, Linear SVR
  • 朴素贝叶斯(Multinomial NB, Gaussian NB
  • 支持向量机(SVC, SVR
  • 全连接神经网络(FCNN-clf, FCNN-reg
  • 一些常用的优化算法(Momentum, Nasterov, RMSProp, Adam)
本篇文章之后的内容大体上是在介绍思路而非实现细节,如果有观众老爷想了解实现细节的话,欢迎在评论区留言,我后续会看时间(与相应细节的受欢迎程度)(说白了就是赞的数目)来写一些相应的文章 ( σ'ω')σ
(等等,这段话是不是有点熟悉

为什么选择使用(或借鉴)carefree-ml ?

首先,我们知道,机器学习(以及现在大行其道的深度学习)算法,很多时候都可以转化为无约束最优化问题;如果不考虑一些特殊性质(稀疏性,收敛速度等)的话,梯度下降法可以说是一种万金油的方法

梯度下降法mlblog.carefree0910.me

因此,carefree-ml 实现的第一大模块,就是一套简单的梯度下降的优化框架,旨在用较少的代码去 handle 大部分情况;在实现一个机器学习算法时,也会优先考虑采用梯度下降算法去实现(这其实就是 carefree-ml 所做的最大的简化了,下面的例子就说明了这一点)

那么,在这种思想下,我们是如何实现 Linear RegressionLogistic Regression 的呢?首先我们可能都知道:

  • 两者都是 线性模型
  • 前者做的是 回归 问题,后者做的是 分类 问题
  • 后者在输出时使用了 sigmoid 激活函数
激活函数一览(第二节)mlblog.carefree0910.me

但是有一点我们可能之前没注意到:

  • 如果前者使用 mse 作为损失函数,后者使用 cross_entropy 作为损失函数,那么它们参数的梯度将会几乎是一模一样的(只差一个倍数)
损失函数一览(第三节)mlblog.carefree0910.me

那么,既然它们如此相似,差异点仅在于几个小部分,它们的实现也应该很像才对。所以,在 carefree-ml 中,它们的实现的主体将分别是:

class LinearRegression(LinearRegressorMixin, RegressorBase):
    def __init__(self):
        self._w = self._b = None

class LogisticRegression(LinearBinaryClassifierMixin, ClassifierBase):
    def __init__(self):
        self._w = self._b = None
        self._sigmoid = Activations("sigmoid")

    def _predict_normalized(self, x_normalized):
        affine = super()._predict_normalized(x_normalized)
        return self._sigmoid(affine)

这里的设计,其实就体现了 carefree-ml 想将机器学习算法 简化 的思想。因为我们知道,mse loss 下的 Linear Regression 是有显式解的(因为就是一个最小二乘法),但是我们仍然用梯度下降去求解它,因为这样它将会与 Logistic Regression 共享大部分代码

当然,这种简化(将许多算法都归结为无约束优化问题并用梯度下降法求解)并不是全是坏处,比如我们完全可以在代码几乎不变的前提下,去求解 l1 loss、或者其它形形色色的 loss 下的 Linear Regression

再比如说 svm

射命丸咲:Python · SVM(一)· 感知机zhuanlan.zhihu.com图标

虽然支持向量分类和支持向量回归看上去是非常不一样的两种算法,但是抽丝剥茧之后,如果用梯度下降法去求解,就会发现其实大部分代码仍然是共享的,这也恰好辅证了为何它们同属 svm 的范畴:

class CoreSVCMixin:
    @staticmethod
    def _preprocess_data(x, y):
        y_svm = y.copy()
        y_svm[y_svm == 0] = -1
        return x, y_svm

    @staticmethod
    def get_diffs(y_batch, predictions):
        return {"diff": 1. - y_batch * predictions, "delta_coeff": -y_batch}

class CoreSVRMixin:
    def get_diffs(self, y_batch, predictions):
        raw_diff = predictions - y_batch
        l1_diff = np.abs(raw_diff)
        if self.eps <= 0.:
            tube_diff = l1_diff
        else:
            tube_diff = l1_diff - self.eps
        return {"diff": tube_diff, "delta_coeff": np.sign(raw_diff)}

class SVCMixin(BinaryClassifierMixin, SVMMixin, metaclass=ABCMeta):
    def predict_prob(self, x):
        affine = self.predict_raw(x)
        sigmoid = Activations.sigmoid(np.clip(affine, -2., 2.) * 5.)
        return np.hstack([1. - sigmoid, sigmoid])

class SVRMixin(SVMMixin, metaclass=ABCMeta):
    def predict(self, x):
        return self.predict_raw(x)

然后真正实现 svm 算法时,就只需继承不同的类即可:

class SVC(CoreSVCMixin, SVCMixin, ClassifierBase):
    def __init__(self,
                 kernel: str = "rbf"):
        self._kernel = Kernel(kernel)

class SVR(CoreSVRMixin, SVRMixin, RegressorBase):
    def __init__(self,
                 eps: float = 0.,
                 kernel: str = "rbf"):
        self._eps = eps
        self._kernel = Kernel(kernel)
当然了,真正的核心代码( SVMMixin)还是要写一写的

此外,除了相似算法间的代码共享,carefree-ml 还致力于常见工程功能上的代码共享。比如说,我们一般可能需要:

  • 对输入的特征进行规范化处理(normalization)
  • 在回归问题中对标签进行 normalization
  • 在二分类问题中通过 roc curve 以及具体的 metric 来挑选出最优分类阈值

这些工程上的东西,也是理应进行代码共享的。因此,carefree-ml 确实在 cfml.models.mixins 中,实现了 NormalizeMixinBinaryClassifierMixin,用于实现这些可能被广泛运用的功能

carefree-ml 能做到什么?

首先,最近其实有很多用 numpy 实现海量算法的 repo,所以单单用 numpy 来作为卖点是不合适的。我个人认为的话,carefree-ml 之所以还算有些特色,主要是由于如下三点:

  • 实现了一个轻量级的、泛用性比较好的梯度下降框架
  • 比起模型的性能,更注重于让算法间共享逻辑、代码;正因此,总代码量会比较少
  • 即使在第二点的“桎梏”下,在小的、比较简单的数据集上,无论是速度还是性能,都是可以锤掉 scikit-learn 的友商产品的

测试方式(包括了安装步骤):

git clone https://github.com/carefree0910/carefree-ml.git
cd carefree-ml
pip install -e .
cd tests/unittests
python test_all.py

在输出中随便截几组数据吧:

~~~  [ info ] timing for    cfml_fcnn     : 0.310764
~~~  [ info ] timing for   sklearn_fcnn   : 0.549960
==========================================================
|             cfml_fcnn  |    mae     |  2.682794  |  <-  
|          sklearn_fcnn  |    mae     |  3.969561  |
----------------------------------------------------------
===========================================================
|             cfml_fcnn  |    mse     |  15.635315  |  <-  
|          sklearn_fcnn  |    mse     |  30.890426  |
-----------------------------------------------------------

~~~  [ info ] timing for     cfml_lr      : 0.039881
~~~  [ info ] timing for    sklearn_lr    : 0.654799
==========================================================
|               cfml_lr  |    auc     |  0.996287  |  <-  
|            sklearn_lr  |    auc     |  0.994675  |
----------------------------------------------------------
==========================================================
|               cfml_lr  |    acc     |  0.980668  |  <-  
|            sklearn_lr  |    acc     |  0.957821  |
----------------------------------------------------------

# gaussian naive bayes
~~~  [ info ] timing for     cfml_gnb     : 0.000000
~~~  [ info ] timing for   sklearn_gnb    : 0.001028
# multinomial naive bayes
~~~  [ info ] timing for     cfml_mnb     : 0.003990
~~~  [ info ] timing for   sklearn_mnb    : 0.007011

~~~  [ info ] timing for     cfml_svc     : 0.207024
~~~  [ info ] timing for    cfml_l_svc    : 0.023937
~~~  [ info ] timing for    sklearn_lr    : 0.571722
~~~  [ info ] timing for   sklearn_svc    : 0.007978
~~~  [ info ] timing for  sklearn_l_svc   : 0.148603
==========================================================
|            cfml_l_svc  |    auc     |  0.996300  |
|              cfml_svc  |    auc     |  1.000000  |  <-  
|            sklearn_lr  |    auc     |  0.994675  |
----------------------------------------------------------
==========================================================
|            cfml_l_svc  |    acc     |  0.985940  |
|              cfml_svc  |    acc     |  1.000000  |  <-  
|         sklearn_l_svc  |    acc     |  0.848858  |
|            sklearn_lr  |    acc     |  0.957821  |
|           sklearn_svc  |    acc     |  0.922671  |
----------------------------------------------------------

~~~  [ info ] timing for     cfml_svr     : 0.090758
~~~  [ info ] timing for    cfml_l_svr    : 0.027925
~~~  [ info ] timing for   sklearn_svr    : 0.008012
~~~  [ info ] timing for  sklearn_l_svr   : 0.165730
==========================================================
|            cfml_l_svr  |    mae     |  3.107422  |  <-  
|              cfml_svr  |    mae     |  5.106989  |
|         sklearn_l_svr  |    mae     |  4.654314  |
|           sklearn_svr  |    mae     |  5.259882  |
----------------------------------------------------------
===========================================================
|            cfml_l_svr  |    mse     |  24.503884  |  <-  
|              cfml_svr  |    mse     |  66.583145  |
|         sklearn_l_svr  |    mse     |  39.598211  |
|           sklearn_svr  |    mse     |  66.818898  |
-----------------------------------------------------------

当然了,吹是这么吹,最后我们还是得负责任地说一句:从实用性、泛化性来说,scikit-learn 肯定都是吊打 carefree-ml 的(比如 carefree-ml 完全不支持稀疏数据)。但是,正如我一开始所说,carefree-ml 只是想探索“机器学习算法到底可以简化成什么样”的产物,所以在简单的数据集上,拟合能力、拟合速度超过 scikit-learn 其实也并不奇怪

注:上述实验结果都是训练集上的结果,所以只能反映拟合能力,不能反映泛化能力

我能怎么使用 carefree-ml?

从实用性角度来看,也许 carefree-ml 实现的那套简易梯度下降框架,是相对而言最实用的。但即便是它,也会被 pytorch 轻松吊锤

所以,正如我开头所说,mainly for educational use,可能教育意义会大于实用意义。虽然本人学术能力不咋地,但是毕竟该 repo 的初衷应该很少搞学术的会看得起并加以研究,所以从这个角度来看,carefree-ml 也许能给观众老爷们带来一些新的体会

值得一提的是,如果不是稀疏的数据集, carefree-ml 的朴素贝叶斯还真的就比 scikit-learn 的快,而且生成出来的模型是一模一样的,欢迎大家试用

声明

虽然我确实用两天就把 carefree-ml 实现了出来(当然后面还有些小修小补),但是这得益于我之前就写过一个用 numpy 实现主流算法的 repo(MachineLearning)(尤其是神经网络的核心代码,其实就是从这个 repo 里面 copy 了过来、稍微改了改就直接跑通了),以及我项目过程中各种搭框架的经验,因此“两天”这个时间还是有挺大的水分在里面的(大体上就是我也想当一回标题党了(喂

写在最后

两天时间就算顶天了也写不出很全面的东西出来,所以 carefree-ml 肯定有很多需要完善的地方,比如加入交叉检验集来用于 Gradient Descent 的 early stop 等。但是,carefree-ml 的出发点毕竟是简化机器学习算法,所以我也不想按照自己的想法去加一些特别细节的功能、而只是先把最基础的功能实现了(比如数据的规范化、二分类问题阈值的选取)

至于其它功能的话,就看这篇文章的反响了;如果时至今日还有观众老爷对这些实现的小细节感兴趣的话,欢迎在评论区留言,我后续会看时间(与相应细节的受欢迎程度)(说白了就是赞的数目)来做一些相应的实现 ( σ'ω')σ(等等,这段话是不是第二次有点熟悉了

虽然这篇文章迷之长、而且干货其实并不多,但还是希望观众老爷们能够喜欢~(凭啥!

如果可以的话,希望观众老爷们能高抬贵手,赏在下一个 star 和 fork ( σ'ω')σ(凭啥!

carefree-mlgithub.com

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!