查看原文
其他

如何入门Python与机器学习 | 赠书

2017-09-04 何宇健 人工智能头条


编者按:本书节选自图书《Python与机器学习实战》

文末评论赠送本书,欢迎留言!


“机器学习”在最近虽可能不至于到人尽皆知的程度,却也是非常火热的词汇。机器学习是英文单词“Machine Learning”(简称ML)的直译,从字面上便说明了这门技术是让机器进行“学习”的技术。然而我们知道机器终究是死的,所谓的“学习”归根结底亦只是人类“赋予”机器的一系列运算。这个“赋予”的过程可以有很多种实现,而Python正是其中相对容易上手、同时性能又相当不错的一门语言。本文打算先谈谈机器学习相关的一些比较宽泛的知识,再介绍并说明为何要使用Python来作为机器学习的工具。最后,我们会提供一个简短易懂的、具有实际意义的例子来给大家提供一个直观的感受。

具体而言,本章主要涉及的知识点有:

  • 机器学习的定义及重要性;

  • Python在机器学习领域的优异性;

  • 如何在电脑上配置Python机器学习的环境;

  • 机器学习一般性的步骤。



机器学习绪论


正如前言所说,由于近期的各种最新成果,使得“机器学习”成为了非常热门的词汇。机器学习在各种领域的优异表现(围棋界的Master是其中最具代表性的存在),使得各行各业的人们都或多或少地对机器学习产生了兴趣与敬畏。然而与此同时,对机器学习有所误解的群体也日益壮大;他们或将机器学习想得过于神秘,或将它想得过于万能。本节拟对机器学习进行一般性的介绍,同时会说明机器学习中一些常见的术语以方便之后章节的叙述。

什么是机器学习

清晨的一句“今天天气真好”、朋友之间的寒暄“你刚刚是去吃饭了吧”、考试过后的感叹“复习了那么久终有收获”……这些日常生活中随处可见的话语,其背后却已蕴含了“学习”的思想—它们都是利用以往的经验、对未知的新情况作出的有效的决策。而把这个决策的过程交给计算机来做,可以说就是“机器学习”的一个最浅白的定义。

我们或许可以先说说机器学习与以往的计算机工作样式有什么不同。传统的计算机如果想要得到某个结果,需要人类赋予它一串实打实的指令,然后计算机就根据这串指令一步步地执行下去。这个过程中的因果关系非常明确,只要人类的理解不出偏差,运行结果是可以准确预测的。但是在机器学习中,这一传统样式被打破了:计算机确实仍然需要人类赋予它一串指令,但这串指令往往不能直接得到结果;相反,它是一串赋予了机器“学习能力”的指令。在此基础上,计算机需要进一步地接受“数据”,并根据之前人类赋予它的“学习能力”,从中“学习”出最终的结果。这个结果往往是无法仅仅通过直接编程得出的。因此这里就导出了稍微深一点的机器学习的定义:它是一种让计算机利用数据而非指令来进行各种工作的方法。在这背后,最关键的就是“统计”的思想,它所推崇的“相关而非因果”的概念是机器学习的理论根基。在此基础上,机器学习可以说是计算机使用输入给它的数据,利用人类赋予它的算法得到某种模型的过程,其最终的目的则是使用该模型,预测未来未知数据的信息。

既然提到了统计,那么一定的数学理论就不可或缺。相关的、比较简短的定义会在第4章给出(PAC框架),这里我们就先只叙述机器学习在统计理论下的、比较深刻的本质:它追求的是合理的假设空间(Hypothesis Space)的选取和模型的泛化(Generalization)能力。该句中出现了一些专用术语,详细的定义会在介绍术语时提及,这里我们提供一个直观的理解:

  • 所谓的假设空间,就是我们的模型在数学上的“适用场合”。

  • 所谓的泛化能力,就是我们的模型在未知数据上的表现。


注意:上述本质上严格来说,应该是PAC Learning的本质;在其余的理论框架下,机器学习是可以具有不同的内核的。


从上面的讨论可以看出,机器学习和人类思考的过程有或多或少的类似。事实上,我们在第6、第7章讲的神经网络(Neural Network,NN)和卷积神经网络(Convolutional Neural Network,CNN)背后确实有着相应的神经科学的理论背景。然而与此同时需要知道的是,机器学习并非是一个“会学习的机器人”和“具有学习能力的人造人”之类的,这一点从上面诸多讨论也可以明晰(惭愧的是,笔者在第一次听到“机器学习”四个字时,脑海中浮现的正是一个“聪明的机器人”的图像,甚至还幻想过它和人类一起生活的场景)。相反的,它是被人类利用的、用于发掘数据背后信息的工具。

当然,现在也不乏“危险的人工智能”的说法,霍金大概是其中的“标杆”,这位伟大的英国理论物理学家甚至警告说“人工智能的发展可能意味着人类的灭亡”。孰好孰坏果然还是见仁见智,但可以肯定的是:本书所介绍的内容绝不至于导致世界的毁灭,大家大可轻松愉快地进行接下来的阅读!

机器学习常用术语

机器学习领域有着许多非常基本的术语,这些术语在外人听来可能相当高深莫测。它们事实上也可能拥有非常复杂的数学背景,但需要知道:它们往往也拥有着相对浅显平凡的直观理解(上一小节的假设空间和泛化能力就是两个例子)。本小节会对这些常用的基本术语进行说明与解释,它们背后的数学理论会有所阐述,但不会涉及过于本质的东西。

正如前文反复强调的,数据在机器学习中发挥着不可或缺的作用;而用于描述数据的术语有好几个,需要被牢牢记住的如下。

  • “数据集”(Data Set),就是数据的集合的意思。其中,每一条单独的数据被称为“样本”(Sample)。若没有进行特殊说明,本书都会假设数据集中样本之间在各种意义下相互独立。事实上,除了某些特殊的模型(如隐马尔可夫模型和条件随机场),该假设在大多数场景下都是相当合理的。

  • 对于每个样本,它通常具有一些“属性”(Attribute)或者说“特征”(Feature),特征所具体取的值就被称为“特征值”(Feature Value)。

  • 特征和样本所张成的空间被称为“特征空间”(Feature Space)和“样本空间”(Sample Space),可以把它们简单地理解为特征和样本“可能存在的空间”。

  • 相对应的,我们有“标签空间”(Label Space),它描述了模型的输出“可能存在的空间”;当模型是分类器时,我们通常会称之为“类别空间”。


其中、数据集又可以分为以下三类:

  • 训练集(Training Set);顾名思义,它是总的数据集中用来训练我们模型的部分。虽说将所有数据集都拿来当作训练集也无不可,不过为了提高及合理评估模型的泛化能力,我们通常只会取数据集中的一部分来当训练集。

  • 测试集(Test Set);顾名思义,它是用来测试、评估模型泛化能力的部分。测试集不会用在模型的训练部分,换句话说,测试集相对于模型而言是“未知”的,所以拿它来评估模型的泛化能力是相当合理的。

  • 交叉验证集(Cross-Validation Set,CV Set);这是比较特殊的一部分数据,它是用来调整模型具体参数的。


注意:需要指出的是,获取数据集这个过程是不平凡的;尤其是当今“大数据”如日中天的情景下,诸如“得数据者得天下”的说法也不算诳语。在此笔者推荐一个非常著名的含有大量真实数据集的网站:http://archive.ics.uci.edu/ml/ datasets.html,本书常常会用到其中一些合适的数据集来评估我们自己实现的模型。


可以通过具体的例子来理解上述概念。比如,我们假设小明是一个在北京读了一年书的学生,某天他想通过宿舍窗外的风景(能见度、温度、湿度、路人戴口罩的情况等)来判断当天的雾霾情况并据此决定是否戴口罩。此时,他过去一年的经验就是他拥有的数据集,过去一年中每一天的情况就是一个样本。“能见度”、“温度”、“湿度”、“路人戴口罩的情况”就是四个特征,而(能见度)“低”、(温度)“低”、(湿度)“高”、(路人戴口罩的)“多”就是相对应的特征值。现在小明想了想,决定在脑中建立一个模型来帮自己做决策,该模型将利用过去一年的数据集来对如今的情况做出“是否戴口罩”的决策。此时小明可以用过去一年中8个月的数据量来做训练集、2个月的量来做测试集、2个月的量来做交叉验证集,那么小明就需要不断地思考(训练模型)下列问题:

  • 用训练集训练出的模型是怎样的?

  • 该模型在交叉验证集上的表现怎么样? 

    • 如果足够好了,那么思考结束(得到最终模型)。

    • 如果不够好,那么根据模型在交叉验证集上的表现,重新思考(调整模型参数)。


最后,小明可能会在测试集上评估自己刚刚思考后得到的模型的性能,然后根据这个性能和模型做出的“是否戴口罩”的决策来综合考虑自己到底戴不戴口罩。

接下来说明上一小节中提到过的重要概念:假设空间与泛化能力。泛化能力的含义在上文也有说明,为强调起见,这里再叙述一遍:

  • 泛化能力针对的其实是学习方法,它用于衡量该学习方法学习到的模型在整个样本空间上的表现。


这一点当然是十分重要的,因为我们拿来训练模型的数据终究只是样本空间的一个很小的采样,如果只是过分专注于它们,就会出现所谓的“过拟合”(Over Fitting)的情况。当然,如果过分罔顾训练数据,又会出现“欠拟合”(Under Fitting)。可以用一张图来直观地感受过拟合和欠拟合(如图1所示,左为欠拟合,右为过拟合)。

图1 欠拟合与过拟合


所以需要“张弛有度”,找到最好的那个平衡点。统计学习中的结构风险最小化(Structural Risk Minimization,SRM)就是研究这个的,它和传统的经验风险最小化(Empirical Risk Minimization,ERM)相比,注重于对风险上界的最小化,而不是单纯地使经验风险最小化。它有一个原则:在使风险上界最小的函数子集中挑选出使经验风险最小的函数。而这个函数子集,正是我们之前提到过的假设空间。

注意:所谓经验风险,可以理解为训练数据集上的风险。相对应的,ERM则可以理解为只注重训练数据集的学习方法,它的理论基础是经验风险在某种足够合理的数学意义上一致收敛于期望风险,亦即所谓的“真正的”风险。


关于SRM和ERM的详细讨论会涉及诸如VC维和正则化的概念,这里不进行详细展开,但需要有这么一个直观的认识:为了使我们学习方法训练出的模型泛化能力足够好,需要对模型做出一定的“限制”,而这个“限制”就表现在假设空间的选取上。一个非常普遍的做法是对模型的复杂度做出一定的惩罚,从而使模型趋于精简。这与所谓的“奥卡姆剃刀原理”不谋而合:“如无必要,勿增实体”“切勿浪费较多的东西去做,用较少的东西、同样可以做好事情”。

相比起通过选取合适的假设空间来规避过拟合,进行交叉验证(Cross Validation)则可以让我们知道过拟合的程度,从而帮助我们选择合适的模型。常见的交叉验证有以下三种。

  • S-fold Cross Validation:中文可翻译成S折交叉验证,它是应用最多的一种方法,其方法大致如下。 

    • 将数据分成S份:D={D_1,D_2,…,D_S},一共做S次试验。

    • 在第i次试验中,使用D-D_i作为训练集,D_i作为测试集对模型进行训练和评测。

    • 最终选择平均测试误差最小的模型。

  • 留一交叉验证(Leave-one-out Cross Validation):这是S折交叉验证的特殊情况,此时S=N。

  • 简易交叉验证:这种实现起来最简单,也是本书(在进行交叉验证时)所采用的方法。它简单地将数据进行随机分组,最后达到训练集约占原数据70%的程度(这个比例可以视情况改变),选择模型时使用测试误差作为标准。


机器学习的重要性

道理说了不少,但到底为什么要学机器学习,机器学习的重要性又在哪里呢?事实上,回顾历史可以发现,人类的发展通常伴随着简单体力劳动向复杂脑力劳动的过渡。过去的工作基本上都有着明确的定义,告诉你这一步怎么做、下一步再怎么做。而如今这一类的工作已经越来越少,取而代之的是更为宽泛模糊的、概念性的东西,比如说“将本季度的产品推向最合适的市场,在最大化期望利润的同时,尽量做到风险最小化”这种需求。想要完成好这样的任务,需要获取相应的数据;虽说网络的存在让我们能够得到数之不尽的数据,然而从这些数据中获得信息与知识却不是一项简单的工作。我们当然可以人工地、仔细地逐项甄选,但这样显然就又回到了最初的原点。机器学习这门技术,可以说正因此应运而生。

单单抽象地说一大堆空话可能会让人头昏脑涨,我们就举一举机器学习具体的应用范围,从中大概能够比较直观地看出机器学习的强大与重要。

发展到如今,机器学习的“爪牙”可谓已经伸展到了各个角落、包括但不限于:

  • 机器视觉,也就是最近机器学习里很火热的深度学习的一种应用;

  • 语音识别,也就是微软Cortana背后的核心技术;

  • 数据挖掘,也就是耳熟能详的大数据相关的领域;

  • 统计学习,也就是本书讲解的主要范围之一,有许许多多著名的算法(比如支持向量机SVM)都源于统计学习(但是统计学习还是和机器学习有区别的;简单地说,统计学习偏数学而机器学习偏实践)。


机器学习还能够进行模式识别、自然语言处理,等等,之前提到过的围棋界的Master和最新人工智能在德州扑克上的表现亦无不呈现着机器学习强大的潜力。一言以蔽之,机器学习是当今的热点,虽说不能保证它的热度能100%地一直延续下去,至少笔者认为、它能在相当长的一段时间内保持强大的生命力。


人生苦短,我用Python


上一节大概地介绍了机器学习的各种概念,这一节我们主要讲讲脚本语言Python相关的一些东西。题目是在Python界流传甚广的“谚语”,它讲述了Python强大的功能与易于上手的特性。

为何选择Python

援引开源运动的领袖人物Eric Raymond的说法:“Python语言非常干净,设计优雅,具有出色的模块化特性。其最出色的地方在于,鼓励清晰易读的代码,特别适合以渐进开发的方式构造项目”。Python的可读性使得即使是刚学不久的人也能看懂大部分的代码,Python庞大的社区和大量的开发文档更是使得初学者能够快速地实现许许多多令人惊叹的功能。对于Python的程序,人们甚至有时会戏称其为“可执行的伪代码(executable pseudo-code)”,以突显它的清晰性和可读性。

Python的强大是毋庸置疑的,上文提到的Eric Raymond甚至称其“过于强大了”。与之相对应的,就是Python的速度比较慢。然而比起Python开发环境提供的海量高级数据结构(如列表、元组、字典、集合等)和数之不尽的第三方库,再加上高速的CPU和近代发展起来的GPU编程,速度的问题就显得没那么尖锐了。况且Python还能通过各种途径使用C / C++代码来编写核心代码,其强大的“胶水”功能使其速度(在程序员能力允许的情况下)和纯粹的C / C++相比已经相去不远。一个典型的例子,也是我们会在本书常常运用到的Python中Numpy这个第三方库。编写它的语言正是底层语言(C和Fortran),其支持向量、矩阵操作的特性和优异的速度,使得Python在科学计算这一领域大放异彩。

注意:Python及本书用到的两个非常优异的第三方库—Numpy和TensorFlow的简要教程我们会作为附录章节放在本书的最后,建议有需要的读者先阅读相应部分。


Python 在机器学习领域的优势

虽然在上一小节叙述了Python的种种好处,但不可否认的是,确实存在诸如MATLAB和Mathematica这样的高级程序语言。它们对机器学习的支持也不错,MATLAB甚至还自带许多机器学习的应用。但是作为一个问心无愧的程序员,我们还是需要提倡支持正版,而MATLAB的正版软件需要花费数千美元。与之相对,由于Python是开源项目,几乎所有必要的组件都是完全免费的。

之前也提到过Python的速度问题,但是更快更底层的语言,比如C和C++,若使用它们来学习机器学习,会不可避免地引发这么一个问题:即使是实现一个非常简单的功能,也需要进行大量的编写和调试的过程;在这期间,程序员很有可能忘掉学习机器学习的初衷而迷失在代码的海洋中。笔者曾经尝试过将Python上的神经网络框架移植到C++上,这之间的折腾至今难忘。

此外,笔者认为、使用Python来学习机器学习是和“不要过早优化”这句编程界的金句有着异曲同工之妙的。Python(几乎)唯一的缺陷—速度,在初期进行快速检验算法、思想正误及开发工作时,其实基本上不是重要问题。其中的道理是显而易见的:如果解决问题的思想存在问题,那么即使拼命去提高程序的运行效率,也只能使问题越来越大而已。这种时候,先使用Python进行快速实现,有必要时再用底层代码重写核心代码,从各方面来说都是一个更好的选择。

关于Anaconda

Python的强大有相当大一部分体现在它那浩如烟海的第三方库。在使用Python实现一个复杂功能时,如果没有特殊的需求,我们通常会先搜索Google有没有现成的第三方库,然后会搜索是否有相关联的第三方库,最后才会考虑自己重头实现。

第三方库是如此之多,从中挑选出心仪而合适的并非易事。幸运的是,就连这一点也有第三方软件进行了支持,那就是在Python科学计算领域非常出名的Anaconda。这是一个完全免费的软件,经常会进行各种更新;最重要的是,它把几乎所有常用且优异的科学计算库都集成在了一起。换句话说,只要你安装了Anaconda,就意味着拥有了一个完善精致的机器学习环境,基本上无须自己把要用到的库一个一个通过命令行来安装。


第一个机器学习样例


作为本章的总结,我们来运用Python解决一个实际问题,以便对机器学习有一个具体的感受。由于该样例只是为了提供直观感受,我们就拿比较有名的一个小问题来进行阐述。俗话说:“麻雀虽小,五脏俱全”,我们完全可以通过这个样例来对机器学习的一般性步骤进行一个大致的认知。

该问题来自Coursera上的斯坦福大学机器学习课程,其叙述如下:现有47个房子的面积和价格,需要建立一个模型对新的房价进行预测。稍微翻译问题,可以得知:

  • 输入数据只有一维,亦即房子的面积。

  • 目标数据也只有一维,亦即房子的价格。

  • 需要做的,就是根据已知的房子的面积和价格的关系进行机器学习。


下面我们就来一步步地进行操作。

获取与处理数据

原始数据集的前10个样本如表1.1所示,这里房子面积和房子价格的单位可以随意定夺,因为它们不会对结果造成影响。

表1.1 房价数据集 

完整的数据集可以参见https://github.com/carefree0910/MachineLearning/blob/master/ _Data/prices.txt。虽然该数据集比较简单,但可以看到其中的数字都相当大。保留它原始形式确实有可能是有必要的,但一般而言,我们应该对它做简单的处理以期望降低问题的复杂度。在这个例子里,采取常用的将输入数据标准化的做法,其数学公式为:

代码1-1 第一个机器学习样例:a_FirstExample\Regression.py

01  # 导入需要用到的库

02  import numpy as np

03  import matplotlib.pyplot as plt

04

05  # 定义存储输入数据(x)和目标数据(y)的数组

06  x, y = [], []

07  # 遍历数据集,变量sample对应的正是一个个样本

08  for sample in open("../_Data/prices.txt", "r"):

09  # 由于数据是用逗号隔开的,所以调用Python中的split方法并将逗号作为参数传入

10      _x, _y = sample.split(",")

11      # 将字符串数据转化为浮点数

12      x.append(float(_x))

13      y.append(float(_y))

14  # 读取完数据后,将它们转化为Numpy数组以方便进一步的处理

15  x, y = np.array(x), np.array(y)

16  # 标准化

17  x = (x - x.mean()) / x.std()

18  # 将原始数据以散点图的形式画出

19  plt.figure()

20  plt.scatter(x, y, c="g", s=6)

21  plt.show()


上面这段代码的运行结果如图2所示。


图2 预处理后的数据散点图


这里横轴是标准化后的房子面积,纵轴是房子价格。以上我们已经比较好地完成了机器学习任务的第一步:数据预处理。

选择与训练模型

在弄好数据之后,下一步就要开始选择相应的学习方法和模型了。幸运的是,通过可视化原始数据,可以非常直观地感受到:很有可能通过线性回归(Linear Regression)中的多项式拟合来得到一个不错的结果。其模型的数学表达式如下。

注意:用多项式拟合散点只是线性回归的很小的一部分,但是它的直观意义比较明显。考虑到问题比较简单,我们才选用了多项式拟合。线性回归的详细讨论超出了本书的范围,这里不再赘述。


其中f(x|p;n)就是我们的模型,p、n都是模型的参数,其中p是多项式f的各个系数,n是多项式的次数。L(p;n)则是模型的损失函数,这里我们采用了常见的平方损失函数,也就是所谓的欧氏距离(或说向量的二范数)。x、y则分别是输入向量和目标向量;在我们这个样例中,x、y这两个向量都是47维的向量,分别由47个不同的房子面积、房子价格所构成。

在确定好模型后,就可以开始编写代码来进行训练了。对于大多数机器学习算法,所谓的训练正是最小化某个损失函数的过程,这个多项式拟合的模型也不例外:我们的目的就是让上面定义的L(p;n)最小。在数理统计领域里有专门的理论研究这种回归问题,其中比较有名的正规方程更是直接给出了一个简单的解的通式。不过由于有Numpy的存在,这个训练过程甚至变得还要更加简单一些。

22  # 在(-2,4)这个区间上取100个点作为画图的基础

23  x0 = np.linspace(-2, 4, 100) 

24  # 利用Numpy的函数定义训练并返回多项式回归模型的函数

25  # deg参数代表着模型参数中的n,亦即模型中多项式的次数

26  # 返回的模型能够根据输入的x(默认是x0),返回相对应的预测的y

27  def get_model(deg):

28      return lambda input_x=x0: np.polyval(np.polyfit(x, y, deg), input_x)


这里需要解释Numpy里面带的两个函数:polyfit和polyval的用法。

  • polyfit(x, y, deg):该函数会返回使得上述(注:该公式中的x和y就是输入的x和y)最小的参数p,亦即多项式的各项系数。换句话说,该函数就是模型的训练函数。

  • polyval(p, x):根据多项式的各项系数p和多项式中x的值,返回多项式的值y。


评估与可视化结果

模型做好后,我们就要尝试判断各种参数下模型的好坏了。为简洁起见,我们采用n=1,4,10这三组参数进行评估。由于我们训练的目的是最小化损失函数,所以用损失函数来衡量模型的好坏似乎是一个合理的做法。

29  # 根据参数n、输入的x、y返回相对应的损失

30  def get_cost(deg, input_x, input_y):

31      return 0.5 * ((get_model(deg)(input_x) - input_y) ** 2).sum()

32  # 定义测试参数集并根据它进行各种实验

33  test_set = (1, 4, 10)

34  for d in test_set:

35      # 输出相应的损失

36      print(get_cost(d, x, y))


所得的结果是:当n=1,4,10时,损失的头两位数字分别为96、94和75。这么看来似乎是n=10优于n=4,而n=1最差,但从图3可以看出,似乎直接选择n=1作为模型的参数才是最好的选择。这里矛盾的来源正是前文所提到过的过拟合情况。


图3 线性回归的可视化


那么,怎么最直观地了解是否出现过拟合了呢?当然还是画图了。

37  # 画出相应的图像

38  plt.scatter(x, y, c="g", s=20)

39  for d in test_set:

40      plt.plot(x0, get_model(d)(), label="degree = {}".format(d))

41  # 将横轴、纵轴的范围分别限制在(-2,4)、(〖10〗^5,8×〖10〗^5)

42  plt.xlim(-2, 4)

43  plt.ylim(1e5, 8e5)

44  # 调用legend方法使曲线对应的label正确显示

45  plt.legend()

46  plt.show()


上面这段代码的运行结果如图3所示。

其中,三条线分别代表n=1、n=4、n=10的情况(图1.10的右上角亦有说明)。可以看出,从n=4开始模型就已经开始出现过拟合现象了,到n=10时模型已经变得非常不合理。

至此,可以说这个问题就已经基本上解决了。在这个样例里面,除了交叉验证,我们涵盖了机器学习中的大部分主要步骤(之所以没有进行交叉验证是因为数据太少了……)。代码部分加起来总共40~50行,应该算是一个比较合适的长度。希望大家能够通过这个样例对机器学习有个大概的了解,也希望它能引起大家对机器学习的兴趣。


小结


  • 与传统的计算机程序不同,机器学习是面向数据的算法,能够从数据中获得信息。它符合新时代脑力劳动代替体力劳动的趋势,是富有生命力的领域。

  • Python是一门优异的语言,代码清晰可读、功能广泛强大。其最大弱点—速度问题也可以通过很多不太困难的方法弥补。

  • Anaconda是Python的一个很好的集成环境,它能让我们免于人工地安装大量科学计算所需要的第三方库。

  • 虽说机器学习算法很多,但通常而言,进行机器学习的过程会包含以下三步: 

    • 获取与处理数据;

    • 选择与训练模型;

    • 评估与可视化结果。


《Python与机器学习实战》订购链接(点击阅读原文订购):https://item.jd.com/12125529.html

又又又又赠书啦!!!

留言告诉头条宝宝你想获得这本书的理由,点赞前5名就可获得本书。

开奖截止时间9月6日(本周三)中午12点!



CSDN AI热衷分享 欢迎扫码关注 

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存