一、构建垃圾邮件分类器
举一个垃圾邮件分类的例子:假如你想建立一个垃圾邮件分类器,假设我们已经有一些加过标签的训练集。
包括标注的垃圾邮件表示为y=1和非垃圾邮件表示为y=0。
我们如何以监督学习的方法来构造一个分类器来区分垃圾邮件和非垃圾邮件呢?
为了应用监督学习,我们首先必须确定的是如何用邮件的特征,构造向量x给出训练集中的特征x和标签y,我们就能够训练出某种分类器,比如用逻辑回归的方法。
这里有一种选择邮件的一些特征变量的方法。比如说我们可能会想出一系列单词能够用来区分垃圾邮件或非垃圾邮件,比如说,如果有封邮件包含单词"deal(交易)" “buy(买)” "discount(折扣)“那么它就很有可能是一封垃圾邮件,如果一封邮件中包含了我的名字"Andrew” 说明这封邮件,不太可能是垃圾邮件。因为某些原因,我认为 "now(现在)"这个单词表明了,这封邮件可能并不是垃圾邮件,因为我经常收到一些很紧急的邮件,当然还有别的单词。
我们可以选出这样成百上千的单词,给出一封这样的邮件,我们可以将这封邮件用一个特征向量来表示,方法如图:
我选择了100个单词,用于表示是否可能为垃圾邮件,所以这个特征向量x 的维度是100 并且 如果这个特定的单词,即单词 j 出现在这封邮件中,那么每一个特征变量 xj 的值为1,反之 xj为0。
虽然我所描述的这个过程是我自己选取的100个单词。但是在实际工作中最普遍的做法是遍历整个训练集。然后,在训练集中(大量的垃圾邮件中),选出出现次数最多的n个单词,n一般介于10,000和50,000之间,然后把这些单词作为你要用的特征。因此不同于手动选取,这些单词会构成特征,这样你就可以用它们来做垃圾邮件分类。
如果你正在构造一个垃圾邮件分类器,你应该会面对这样一个问题,那就是:你最该去使用哪一种改进你的方法,从而使得你的垃圾邮件分类器具有较高的准确度。从直觉上讲,是要收集大量的数据,生成这个叫做 data 的对象,是吧? 事实上确实好多人这么做,很多人认为收集越多的数据算法就会表现的越好。
就垃圾邮件分类而言,有一个叫做"Honey Pot"的项目。它可以建立一个假的邮箱地址,故意将这些地址泄露给发垃圾邮件的人,这样就能收到大量的垃圾邮件。你看,这样的话,我们就能得到非常多的垃圾邮件来训练学习算法,但是,在前面的课程中我们知道大量的数据可能会有帮助,也可能没有。
对于大部分的机器学习问题,还有很多办法用来提升机器学习的效果。比如对于垃圾邮件而言 也许你会想到用更复杂的特征变量,像是邮件的路径信息。这种信息通常会出现在邮件的标题中。因此,垃圾邮件发送方在发送垃圾邮件时,他们总会试图让这个邮件的来源变得模糊一些。或者是用假的邮件标题,或者通过不常见的服务器来发送邮件,用不常见的路由,他们就能给你发送垃圾邮件,而且这些信息也有可能包含在邮件标题部分,因此可以想到,我们可以通过邮件的标题部分来构造更加复杂的特征,来获得一系列的邮件路由信息,进而判定这是否是一封垃圾邮件。
你还可能会想到别的方法,比如,从邮件的正文出发,寻找一些复杂点的特征,例如:单词"discount" 是否和单词"discounts"是一样的。又比如,单词"deal(交易)"和"dealer(交易商)"是否也应视为等同。甚至,像这个例子中,有的单词小写有的大写,或者我们是否应该用标点符号来构造复杂的特征变量。因为垃圾邮件可能会更多的使用感叹号,这些都不一定。
同样的,我们也可能构造更加复杂的算法来检测或者纠正那些故意的拼写错误。例如"m0rtgage" “med1cine” “w4tches” 。因为垃圾邮件发送方确实这么做了,因为如果你将4放到"w4tches"中,那么,用我们之前提到的简单的方法,垃圾邮件分类器不会把"w4tches" 和"watches" 看成一样的,这样我们就很难区分这些故意拼错的垃圾邮件。发垃圾邮件的也很机智,他们这么做就逃避了一些过滤。当我们使用机器学习时,总是可以“头脑风暴”一下 想出一堆方法来试试,就像这样。
顺带一提,我有一段时间,研究过垃圾邮件分类的问题,实际上我花了很多时间来研究这个。尽管我能够理解垃圾邮件分类的问题,我确实懂一些这方面的东西,但是,我还是很难告诉你 这四种方法中,你最该去使用哪一种。事实上,坦白地说,最常见的情况是一个研究小组可能会随机确定其中的一个方法。但是有时候这种方法并不是最有成效的,你知道你只是随机选择了其中的一种方法。
实际上,当你需要通过头脑风暴来想出不同方法来尝试去提高精度的时候 你可能已经超越了很多人了。令人难过的是:大部分人他们并不尝试着列出可能的方法。他们做的只是某天早上醒来,因为某些原因,有了一个突发奇想 “让我们来试试,用Honey Pot项目 收集大量的数据吧”。 不管出于什么奇怪的原因,早上的灵机一动,还是随机选一个然后干上大半年,但是我觉得我们有更好的方法。
下一节,我们将通过误差分析告诉你怎样用一个更加系统性的方法从一堆不同的方法中选取合适的那一个。
二、误差分析
如果你准备研究机器学习的东西或者构造机器学习应用程序。最好的实践方法不是建立一个 非常复杂的系统,拥有多么复杂的变量,而是构建一个简单的算法。这样你可以很快地实现它。 每当我研究机器学习的问题时,我最多只会花一天的时间,来试图很快的把结果搞出来。 即便效果不好,根本没有用复杂的系统,但是只是很快的得到的结果,即便运行得不完美,但是也把它运行一遍。最后通过交叉验证来检验数据。
一旦做完,再通过画出学习曲线以及检验误差,来找出你的算法是否有高偏差和高方差的问题,或者别的问题。在这样分析之后,再来决定用更多的数据训练或者加入更多的特征变量是否有用。
这么做的原因是,这在你刚接触机器学习问题时,是一个很好的方法。你并不能提前知道你是否需要复杂的特征变量或者你是否需要更多的数据,还是别的什么,提前知道你应该做什么 是非常难的,因为你缺少证据,缺少学习曲线。因此,你很难知道你应该把时间花在什么地方来提高算法的表现。但是当你实践一个非常简单即便不完美的方法时,你可以通过画出学习曲线来做出进一步的选择。
这种理念是我们必须用证据来领导我们的决策,怎样分配自己的时间来优化算法,而不是仅仅凭直觉。凭直觉得出的东西一般总是错误的,除了画出学习曲线之外,一件非常有用的事是误差分析。
当我们在构造比如构造垃圾邮件分类器时,我会看一看,我的交叉验证数据集,然后亲自看一看哪些邮件被算法错误地分类。因此,通过这些被算法错误分类的垃圾邮件与非垃圾邮件,你可以发现某些系统性的规律,什么类型的邮件总是被错误分类。经常地,这样做之后,这个过程能启发你构造新的特征变量或者告诉你现在这个系统的短处。然后启发你如何去提高它。
具体地说,这里有一个例子。假设你正在构造一个垃圾邮件分类器。你拥有500个实例,在交叉验证集中,假设在这个例子中,该算法有非常高的误差率。它错误分类了一百个交叉验证实例。所以我要做的是人工检查这100个错误,然后手工为它们分类。基于例如:这些是什么类型的邮件,哪些变量能帮助这个算法来正确分类它们。明确地说通过鉴定这是哪种类型的邮件, 通过检查这一百封错误分类的邮件,我可能会发现最容易被误分类的邮件。可能是有关药物的邮件,基本上这些邮件都是卖药的,或者卖仿品的,比如卖假表。或者一些骗子邮件 又叫做钓鱼邮件等等。所以,在检查哪些邮件被错误分类的时候,我会看一看每封邮件,数一数,比如,在这100封错误归类的邮件中,我发现有12封错误归类的邮件是和卖药有关的邮件。 4封是推销仿品的 推销假表或者别的东西。然后我发现有53封邮件是钓鱼邮件,诱骗你告诉他们你的密码。剩下的31封别的类型的邮件。通过算出每个类别中不同的邮件数,你可能会发现比如该算法在区分钓鱼邮件的时候,总是表现得很差。这说明你应该花更多的时间来研究这种类型的邮件,然后,看一看你是否能通过构造更好的特征变量来正确区分这种类型的邮件。同时,我要做的是看一看哪些特征变量 可能会帮助算法正确地分类邮件。
我们假设能帮助我们提高邮件分类表现的方法是:检查有意的拼写错误,不寻常的邮件路由来源,以及垃圾邮件特有的标点符号方式,比如很多感叹号。与之前一样,我会手动地浏览这些邮件,假设有5封这种类型的邮件,16封这种类型的,32封这种类型的,以及一些别的类型的。 如果,这就是你从交叉验证中得到的结果,那么,这可能说明有意地拼写错误出现频率较少。这可能并不值得你花费时间去编写算法来检测这种类型的邮件,但是如果你发现很多的垃圾邮件都有不一般的标点符号规律,那么这是一个很强的特征,说明你应该花费你的时间 去构造基于标点符号的更加复杂的特征变量。因此,这种类型的误差分析是一种手动检测的过程。检测算法可能会犯的错误经常能够帮助你找到更为有效的手段,这也解释了为什么我总是推荐先实践一种快速即便不完美的算法。我们真正想要的是找出什么类型的邮件,是这种算法最难分类出来的。对于不同的算法,不同的机器学习算法,它们所遇到的问题一般总是相同的,通过实践一些快速即便不完美的算法。你能够更快地找到错误的所在,并且快速找出算法难以处理的特例中。这样你就能集中精力在这些真正的问题上。
最后,在构造机器学习算法时,另一个有用的小窍门是保证你自己能有一种数值计算的方式来评估你的机器学习算法。
先看看这个例子 假设我们试图决定是否应该把像"discount"“discounts”“discounter”“discountring” 这样的单词都视为等同一种方法,是检查这些单词的开头几个字母。比如,当你在检查这些单词开头几个字母的时候,你发现这几个单词大概可能有着相同的意思。
在自然语言处理中,这种方法是通过一种叫做词干提取的软件实现的,如果你想自己来试试,你可以在网上搜索一下 “Porter Stemmer(波特词干提取法)” 这是在词干提取方面一个比较不错的软件。这个软件会将单词"discount""discounts"以及等等都视为同一个单词。但是这种词干提取软件只会检查单词的头几个字母,这有用。
但是也可能会造成一些问题。因为:举个例子,因为这个软件会把单词"universe(宇宙)“和"university(大学)” 也视为同一个单词。因为这两个单词开头的字母是一样的。因此,当你在决定是否应该使用词干提取软件用来分类,这总是很难说清楚特别地误差分析,也并不能帮助你决定词干提取是不是一个好的方法。
与之相对地,最好的方法,来发现词干提取软件对你的分类器到底有没有用,是迅速地着手试一试来看看它表现到底怎么样,为了这么做通过数值来评估你的算法是非常有用的。具体地说,自然而然地你应该通过交叉验证来验证不用词干提取与用词干提取的算法的错误率。因此,如果你不在你的算法中使用词干提取,然后你得到比如5%的分类错误率,然后你再使用词干提取来运行你的算法,你得到比如3%的分类错误。那么这很大的减少了错误发生,于是你决定词干提取是一个好的办法,就这个特定的问题而言。这里有一个数量的评估数字,即交差验证错误率,我们以后会发现这个例子中的评估数字还需要一些处理,但是我们可以在今后的课程中看到这么做还是会让你能更快地做出决定,比如是否使用词干提取。
如果每一次你实践新想法的时候,你都手动地检测这些例子,去看看是表现差还是表现好,那么这很难让你做出决定到底是否使用词干提取,是否区分大小写。但是通过一个量化的数值评估,你可以看看这个数字,误差是变大还是变小了。
你可以通过它更快地实践你的新想法,它基本上非常直观地告诉你,你的想法是提高了算法表现,还是让它变得更坏?这会大大提高你实践算法时的速度,所以我强烈推荐在交叉验证集上来实施误差分析,而不是在测试集上。但是还是有一些人会在测试集上来做误差分析,即使这从数学上讲,是不合适的。所以我还是推荐你在交叉验证向量上来做误差分析。
总结一下:
当你在研究一个新的机器学习问题时,我总是推荐你实现一个较为简单快速,即便不是那么完美的算法。我几乎从未见过人们这样做,大家经常干的事情是:花费大量的时间在构造算法上 构造他们以为的简单的方法。因此,不要担心你的算法太简单或者太不完美,而是尽可能快地 实现你的算法。当你有了初始的实现之后,它会变成一个非常有力的工具来帮助你决定下一步的做法。因为我们可以先看看算法造成的错误,通过误差分析来看看他犯了什么错,然后来决定优化的方式。另一件事是:假设你有了一个快速而不完美的算法实现,又有一个数值的评估数据,这会帮助你尝试新的想法,快速地发现你尝试的这些想法是否能够提高算法的表现,从而,你会更快地做出决定:在算法中放弃什么、采纳什么。
三、处理偏斜数
有了算法的评估和误差度量值,有一件重要的事情要注意:就是使用一个合适的误差度量值,这有时会对于你的学习算法造成非常微妙的影响。这件重要的事情就是偏斜类(skewed classes)的问题:
比如癌症分类问题:我们拥有内科病人的特征变量,我们希望知道他们是否患有癌症,我们假设 y=1表示患者患有癌症,假设 y=0 表示他们没有得癌症。
我们训练逻辑回归模型,假设我们用测试集,检验了这个分类模型,并且发现它只有1%的错误,因此我们99%会做出正确诊断。看起来是非常不错的结果,但是我们发现在测试集中只有0.5%的患者真正得了癌症,因此在这个例子中 1%的错误率就不再显得那么好了。
举个具体的例子,这里有一行代码,它让y总是等于0,因此它总是预测:没有人得癌症。那么这个算法实际上只有 0.5%的错误率。因此这甚至比我们之前得到的1%的错误率更好。在这个例子中,正样本的数量与负样本的数量相比非常非常少,我们把这种情况叫做偏斜类。一个类中的样本数与另一个类的数据相比,多很多。通过总是预测y=0 或者总是预测y=1算法可能表现非常好。
因此使用分类误差或者分类精确度来作为评估度量可能会产生如下问题:
假如说你有一个算法,它的精确度是99.2% 。因此它只有0.8%的误差,假设你对你的算法做出了一点改动,现在你得到了 99.5%的精确度,只有0.5%的误差。这到底是不是算法的一个提升呢?用某个实数来作为评估度量值的一个好处就是:它可以帮助我们迅速决定我们是否需要对算法做出一些改进,将精确度从99.2%提高到99.5% 。但是我们的改进到底是有用的,还是说我们只是把代码替换成了。例如总是预测y=0 这样的东西,因此如果你有一个偏斜类,用分类精确度并不能很好地衡量算法。因为你可能会获得一个很高的精确度,非常低的错误率。 但是我们并不知道我们是否真的提升了分类模型的质量。因为总是预测y=0 并不是一个好的分类模型。但是总是预测y=0 会将你的误差降低至比如降低至0.5%。
当我们遇到这样一个偏斜类时,我们希望有一个不同的误差度量值,其中一种评估度量值叫做查准率(precision)和召回率(recall。让我来解释一下假设我们正在用测试集来评估一个二元分类模型,我们的学习算法要做的是做出值的预测。如果有一个样本它实际所属的类是1,预测的类也是1,那么,我们把这个样本叫做真阳性(true positive)学习算法,预测某个值是阴性,等于0,实际的类也确实属于0,那么我们把这个叫做真阴性(true negative) 学习算法。预测某个值等于1,但是实际上它等于0,这个叫做假阳性(false positive)。算法预测值为0,但是实际值是1,叫做假阴性(false negative)。这样,我们有了一个2x2的表格 基于实际类与预测类,这样我们有了一个另一种方式来评估算法的表现。
我们要计算两个数字,第一个叫做查准率,这个意思是对于所有我们预测,他们患有癌症的病人有多大比率的病人是真正患有癌症的。
一个分类模型的查准率 = 真阳性/预测为阳性 = 真阳性/(真阳性 +假阳性)
查准率越高就越好 。
另一个数字我们要计算的,叫做召回率。召回率是,如果所有这些在数据集中的病人确实得了癌症,有多大比率我们正确预测他们得了癌症。
召回率 = 真阳性/实际阳性 = 真阳性/(真阳性 + 假阴性)
同样地,召回率越高越好。
通过计算查准率和召回率,我们能更好的知道:分类模型到底好不好。
四、临界值
癌症分类的例子,假如我们希望在我们非常确信地情况下,才预测一个病人得了癌症,这样做的一种方法是修改算法。我们不再将临界值设为0.5,也许,我们只在 h(x)的值大于或等于0.7 的情况下才预测y=1,因此你的回归模型会有较高的查准率和较低的召回率。
因为,当我们做预测的时候,我们只给很小一部分的病人预测y=1。现在我们把这个情况夸大一下,我们临界值设为0.9,我们只在至少90%肯定这个病人患有癌症的情况下,预测y=1。那么这些病人当中有非常大的比率真正患有癌症,因此这是一个高查准率的模型。但是召回率会变低,因为我们希望能够正确检测患有癌症的病人。
现在考虑一个不同的例子。假设我们希望避免遗漏掉患有癌症的人,即我们希望避免假阴性。具体地说,如果一个病人实际患有癌症,但是我们并没有告诉他患有癌症,那这可能造成严重后果。在这个例子中,我们将临界值设得较低,比如0.3。在这种情况下,我们会有一个较高召回率和较低的查准率的模型。
因此,总的来说,对于大多数的回归模型,你得权衡查准率和召回率:
当你改变临界值的值时,我在这儿画了一个临界值,你可以画出曲线来权衡查准率和召回率。 这里的一个值反应出一个较高的临界值,这个临界值可能等于0.99。我们假设只在有大于99%的确信度的情况下才预测y=1,至少有99%的可能性。因此这个点反应高查准率低召回率。然而这里的一个点反映一个较低的临界值,比如说0.01。毫无疑问,在这里预测y=1。如果你这么做,你最后会得到很低的查准率,但是较高的召回率。
当你改变临界值,如果你愿意,你可以画出回归模型的所有曲线来看看你能得到的查准率和召回率的范围。顺带一提,查准率-召回率曲线可以是各种不同的形状,有时它看起来是这样,有时是那样。查准率-召回率曲线的形状有很多可能性,这取决于回归模型的具体算法。因此这又产生了另一个有趣的问题,那就是有没有办法自动选取临界值或者更广泛地说,如果我们有不同的算法或者不同的想法,我们如何比较不同的查准率和召回率呢?
具体来说,假设我们有三个不同的学习算法,或者这三个不同的学习曲线是同样的算法但是临界值不同。我们怎样决定哪一个算法是最好的,我们之前讲到的其中一件事就是评估度量值的重要性。
这个概念是通过一个具体的数字来反映你的回归模型到底如何。但是查准率和召回率的问题,我们却不能这样做。因为在这里我们有两个可以判断的数字。因此,我们经常会不得不面对这样的情况,如果我们正在试图比较算法1和算法2,我们最后问自己到底是0.5的查准率与 0.4的召回率好,还是说 0.7的查准率与 0.1的召回率好,或者每一次你设计一个新算法, 你都要坐下来思考:到底0.5好,还是0.4好?还是说0.7好,还是0.1好?如果你最后这样坐下来思考,这会降低你的决策速度:思考到底哪些改变是有用的应该被融入到你的算法。与此相反的是,如果我们有一个评估度量值,一个数字能够告诉我们到底是算法1好还是算法2好。这能够帮助我们更快地决定哪一个算法更好,同时也能够更快地帮助我们评估不同的改动中哪些应该被融入进算法里面,那么我们怎样才能得到这个评估度量值呢?
你可能会去尝试的一件事情是计算一下查准率和召回率的平均值,用 P 和 R 来表示查准率和召回率,你可以做的是计算它们的平均值,看一看哪个模型有最高的均值。但是这可能并不是一个很好的解决办法,因为像我们之前的例子一样,如果我们的回归模型总是预测 y=1 这么做你可能得到非常高的召回率,得到非常低的查准率。相反地,如果你的模型总是预测y=0 就是说,如果很少预测y=1,对应的,设置了一个高临界值。最后,你会得到非常高的查准率和非常低的召回率。
这两个极端情况:一个有非常高的临界值,一个有非常低的临界值,它们中的任何一个都不是一个好的模型。
相反地,有一种结合查准率和召回率的不同方式,叫做F值,公式是这样。在这个例子中,F值是这样的。我们可以通过F值来判断算法1有最高的F值,算法2第二,算法3是最低的。因此,通过F值,我们会在这几个算法中选择算法1,F值也叫做F1值,一般写作F1值,但是人们一般只说F值,它的定义会考虑一部分查准率和召回率的平均值。但是它会给查准率和召回率中较低的值更高的权重。
因此,你可以看到F值的分子是查准率和召回率的乘积,因此如果查准率等于0,或者召回率等于0,F值也会等于0。因此它结合了查准率和召回率,对于一个较大的F值,查准率和召回率都必须较大。
我必须说,有较多的公式可以结合查准率和召回率,F值公式只是其中一个。但是出于历史原因和习惯问题,人们在机器学习中使用F值,这个术语F值,没有什么特别的意义,所以不要担心,它到底为什么叫做F值或者F1值,但是它给了你你需要的有效方法。
因为无论是查准率等于0,还是召回率等于0,它都会得到一个很低的F值。因此 如果要得到一个很高的F值,你的算法的查准率和召回率都要接近于1。具体地说,如果P=0或者 R=0,你的F值也会等于0,对于一个最完美的F值,如果查准率等于1,同时召回率也等于1, 那你得到的F值等于1乘以1除以2再乘以2。那么F值就等于1,如果你能得到最完美的查准率和召回率,在0和1中间的值,这经常是回归模型最经常出现的分数。
在这次的视频中,我们讲到了如何权衡查准率和召回率,以及我们如何变动临界值来决定我们希望预测y=1还是y=0,比如我们需要一个70%还是90%置信度的临界值或者别的来预测y=1,通过变动临界值,你可以控制权衡查准率和召回率之后我们讲到了F值,它权衡查准率和召回率,给了你一个评估度量值,当然,如果你的目标是自动选择临界值来决定你希望预测y=1 还是y=0,那么一个比较理想的办法是评估这些不同的临界值,在交叉检验集上得到最高的F值,这是自动选择临界值的较好办法。
五、 使用大型数据集
事实证明,在一定条件下,得到大量的数据,并在某种类型的学习算法中进行训练。可以是一种有效的方法来获得一个具有良好性能的学习算法。而这种情况往往出现在这些条件对于你的问题都成立,并且你能够得到大量数据的情况下,这可以是一个很好的方式来获得非常高性能的学习算法。
我先讲一个故事:很多很多年前,我认识的两位研究人员Michele Banko 和 Eric Brill 进行了一项有趣的研究。他们感兴趣的是研究使用不同的学习算法的效果,与将这些效果使用到不同训练数据集上,两者的比较,他们当时考虑这样一个问题,如何在易混淆的词之间进行分类。
比如,在这样的句子中: 早餐我吃了一个鸡蛋 (to,two,too) 在这个例子中,早餐我吃了2个鸡蛋。这是一个易混淆的单词的例子。而这是另外一组情况,于是他们把诸如这样的机器学习问题当做一类监督学习问题,并尝试将其分类。什么样的词在一个英文句子特定的位置才是合适的。
他们选了四种分类算法,这些具体算法并不重要,他们所做的就是改变了训练数据集的大小,并尝试将这些学习算法用于不同大小的训练数据集中,这就是他们得到的结果。这些趋势非常明显,首先大部分算法都具有相似的性能。其次,随着训练数据集的增大,这些算法的性能也都对应地增强了。
事实上,如果你选择选择了一个"劣等的"算法,如果你给这个劣等算法更多的数据。那么,它有可能会比"优等算法"更好,像这样的结果引起了一种在机器学习中的普遍共识: “取得成功的人不是拥有最好算法的人,而是拥有最多数据的人” 。那么这种说法在什么时候是真,什么时候是假呢?
如果你有大量的数据,而且你训练了一种带有很多参数的学习算法,那么这将会是一个很好的方式来提供一个高性能的学习算法。
我觉得关键的测试,我常常问自己,首先一个人类专家看到了特征值 x 能很有信心的预测出 y值吗? 因为这可以证明 y 可以根据特征值 x 被准确地预测出来。其次,我们实际上能得到一组庞大的训练集并且在这个训练集中训练一个有很多参数的学习算法吗? 如果你不能做到这两者,那么更多时候。你会得到一个性能很好的学习算法。
参考资料:10 机器学习(吴恩达):机器学习系统设计
吴恩达机器学习