零基础入门深度学习 - yunfanfan/Notes GitHub Wiki
人工智能领域,有一个方法叫机器学习。在机器学习这个方法里,有一类算法叫神经网络。(人工智能>机器学习>神经网络)
神经网络如图:
上图中每个圆圈都是一个神经元,每条线表示神经元之间的连接。我们可以看到,上面的神经元被分成了多层,层与层之间的神经元有连接,而层内之间的神经元没有连接。最左边的层叫做输入层,这层负责接收输入数据;最右边的层叫输出层,我们可以从这层获取神经网络输出数据。输入层和输出层之间的层叫做隐藏层。
隐藏层比较多(大于2)的神经网络叫做深度神经网络。而深度学习,就是使用深层架构(比如,深度神经网络)的机器学习方法。
深层网络和浅层网络相比优势在于:深层网络能够表达力更强。
- 一个仅有一个隐藏层的神经网络就能拟合任何一个函数,但是它需要很多很多的神经元。
- 深层网络用少得多的神经元就能拟合同样的函数。
- 为了拟合一个函数,要么使用一个浅而宽的网络,要么使用一个深而窄的网络。而后者往往更节约资源。
- 深层网络也有劣势,就是它不太容易训练。简单的说,你需要大量的数据,很多的技巧才能训练好一个深层网络。
下图是一个感知器:
可以看到,一个感知器有如下组成部分:
我们令w1=0.5; w2=0.5; b=-0.8,而激活函数就是前面写出来的阶跃函数,这时,感知器就相当于and
函数。
$$
\begin{aligned}
y &=f(\mathrm{w} \bullet \mathrm{x}+b) \
&=f\left(w_{1} x_{1}+w_{2} x_{2}+b\right) \
&=f(0.5 \times 0+0.5 \times 0-0.8) \
&=f(-0.8) \
&=0
\end{aligned}
$$
感知器可以拟合任何线性
函数,任何线性分类或线性回归问题都可以用感知器来解决。
其中: $$ \begin{aligned} \Delta w_{i} &=\eta(t-y) x_{i} \ \Delta b &=\eta(t-y) \end{aligned} $$
wi是与输入xi对应的权重,b是偏执项。t是训练样本的实际值,一般称之为label。而是感知器的输出值,它是根据公式(1)计算得出。$\eta$是一个称为学习速率的常数,其作用是控制每一步调整权的幅度。
每次从训练数据中取出一个样本的输入向量x,使用感知器计算其输出y,再根据上面的规则来调整权重。每处理一个样本就调整一次权重。经过多轮迭代后(即全部的训练数据被反复处理多轮),就可以训练出感知器的权重,使之实现目标函数。
原代码用python2.7实现:
我尝试使用python3.7来实现:
# -*- coding: utf-8 -*-
from functools import reduce
class Perceptron(object):
def __init__(self, inputNum, activator):
'''
:param inputNum: 输入参数个数
:param activator: 激活函数
'''
self.activator = activator
# 权重向量初始化为0,_被称为丢弃变量,用完就不需要了
self.weights = [0.0 for _ in range(inputNum)]
# 偏置项初始化为0
self.bias = 0.0
def __str__(self):
'''
str方法用于打印类的实例对象时被调用,一般返回一个字符串
:return: 学习到的权重、偏置项
'''
return 'weights\t:%s\nbias\t:%f\n' % (self.weights, self.bias)
def predict(self, input_vec):
'''
:param input_vec: 输入向量
:return: 感知器的计算结果
'''
# 把input_vec[x1,x2,x3...]和weights[w1,w2,w3,...]打包在一起
# 变成[(x1,w1),(x2,w2),(x3,w3),...]
# 然后利用map函数计算[x1*w1, x2*w2, x3*w3]
# 最后利用reduce求和
return self.activator(
reduce(lambda a, b: a + b,
map(lambda x, w: x * w,
input_vec, self.weights)
, 0.0) + self.bias)
def train(self, input_vecs, labels, iteration, rate):
'''
:param input_vecs: 一组向量
:param labels: 与每个向量对应的label
:param iteration: 训练轮数
:param rate: 学习率
'''
for i in range(iteration):
self._one_iteration(input_vecs, labels, rate)
def _one_iteration(self, input_vecs, labels, rate):
'''
:return: 一次迭代,把所有训练数据过一遍
'''
# 把输入和输出打包在一起,成为样本的列表[(input_vec, label), ...]
# 而每个训练样本是(input_vec, label)
samples = zip(input_vecs, labels)
# 对每个样本,按照感知器规则更新权重
for (input_vec, label) in samples:
# 计算感知器在当前权重下的输出
output = self.predict(input_vec)
# 更新权重
self._update_weights(input_vec, output, label, rate)
def _update_weights(self, input_vec, output, label, rate):
'''
按照感知器规则更新权重
'''
# 把input_vec[x1,x2,x3,...]和weights[w1,w2,w3,...]打包在一起
# 变成[(x1,w1),(x2,w2),(x3,w3),...]
# 然后利用感知器规则更新权重
delta = label - output
self.weights = list(map(
lambda x, w: w + rate * delta * x,
input_vec, self.weights))
# 更新bias
self.bias += rate * delta
接下来,我们利用这个感知器类去实现and
函数。
def f(x):
'''
定义激活函数f
'''
return 1 if x > 0 else 0
def get_training_dataset():
'''
基于and真值表构建训练数据
'''
# 构建训练数据
# 输入向量列表
input_vecs = [[1,1], [0,0], [1,0], [0,1]]
# 期望的输出列表,注意要与输入一一对应
# [1,1] -> 1, [0,0] -> 0, [1,0] -> 0, [0,1] -> 0
labels = [1, 0, 0, 0]
return input_vecs, labels
def train_and_perceptron():
'''
使用and真值表训练感知器
'''
# 创建感知器,输入参数个数为2,激活函数为f
p = Perceptron(2, f)
# 训练 迭代10轮,学习速率为0.1
input_vecs, labels = get_training_dataset()
p.train(input_vecs, labels, 10, 0.1)
return p
if __name__ == '__main__':
# 训练and感知器
and_perception = train_and_perceptron()
# 打印训练获得的权重
print(and_perception)
# 测试
print('1 and 1 = %d' % and_perception.predict([1, 1]))
print('0 and 0 = %d' % and_perception.predict([0, 0]))
print('1 and 0 = %d' % and_perception.predict([1, 0]))
print('0 and 1 = %d' % and_perception.predict([0, 1]))
以上代码可以在python3环境里顺利运行。
代码说明(python2与3区别):
- zip 方法在 Python 2 和 Python 3 中的不同:在 Python 3.x 中为了减少内存,zip() 返回的是一个对象。如需展示列表,需手动 list() 转换。
- map 方法在Python 2.x 返回列表。Python 3.x 返回迭代器。如果需要表示列表,加上list(map())。
- print这个是最基本的区别,2不加括号,3要加括号。
简单说一下遇到的小问题:
第一次运行出来是有错的,并没有学习到and函数
,排查了一下是因为_update_weights(self, input_vec, output, label, rate):
函数的问题。
self.weights = list(map(
lambda x, w: w + rate * delta * x,
input_vec, self.weights))
重点是map
前的list
,第一次想着前面predict(self, input_vec)
的map没有加list,这里也就没有加上。问题就在于map
在python3
里返回的是一个迭代器,把迭代器给了self.weights
第二次就无法正常运算了,而前面predict
函数可以是因为map之后没有直接赋值,而是又参与了reduce
运算,reduce
可以接收迭代器,所以第一次不需要转成list
。
在这里加上list后,就能成功运算了。如图:
在这第一节里通过实例代码学到了很多东西,比如zip,map,reduce函数。还有体会了一下简单的训练模型的过程,十分享受。
零基础入门深度学习(1) - 感知器 - 作业部落 Cmd Markdown 编辑阅读器
感知器无法对线性不可分的数据集收敛。为了解决这个问题,我们使用一个可导的线性函数来替代感知器的阶跃函数,这种感知器就叫做线性单元。线性单元在面对线性不可分的数据集时,会收敛到一个最佳的近似上。
简单点,我们可以设置线性单元的激活函数为$f$为 $$ f(x)=x $$ 这样的线性单元如下图所示
对比前面的感知器
这样替换了激活函数$f$之后,线性单元将返回一个实数值而不是0,1分类。因此线性单元用来解决回归问题而不是分类问题。
ps: 啥是“回归问题”?啥又是“分类问题”?
输入变量与输出变量均为连续变量的预测问题是回归问题; 输出变量为有限个离散变量的预测问题成为分类问题; 输入变量与输出变量均为变量序列的预测问题成为标注问题。
摘自:李航《统计学习方法》p4
分类和回归的区别在于输出变量的类型。
定量输出称为回归,或者说是连续变量预测; 定性输出称为分类,或者说是离散变量预测。
举个例子: 预测明天的气温是多少度,这是一个回归任务; 预测明天是阴、晴还是雨,就是一个分类任务。
当我们说模型时,我们实际上在谈论根据输入$x$预测输出$y$的算法。比如,$x$可以是一个人的工作年限,$y$可以是他的月薪,我们可以用某种算法来根据一个人的工作年限来预测他的收入。比如: $$ y=h(x)=w * x+b $$ 函数$h(x)$叫做假设,而$w$、$b$是它的参数。我们假设参数$w=1000$,参数$b=500$,如果一个人的工作年限是5年的话,我们的模型会预测他的月薪为 $$ y=h(x)=1000 * 5+500=5500(元) $$ 你也许会说,这个模型太不靠谱了。是这样的,因为我们考虑的因素太少了,仅仅包含了工作年限。如果考虑更多的因素,比如所处的行业、公司、职级等等,可能预测就会靠谱的多。我们把工作年限、行业、公司、职级这些信息,称之为特征。对于一个工作了5年,在IT行业,百度工作,职级T6这样的人,我们可以用这样的一个特征向量来表示他 $$ \mathrm{x}=(5,IT,百度, T 6) $$ 既然输入$x$变成了一个具备四个特征的向量,相对应的,仅仅一个参数$w$就不够用了,我们应该使用4个参数$w_1, w_2, w_3, w_4$,每个特征对应一个。这样,我们的模型就变成 $$ y=h(x)=w_{1} * x_{1}+w_{2} * x_{2}+w_{3} * x_{3}+w_{4} * x_{4}+b $$ 其中,$x_1$对应工作年限,$x_2$对应行业,$x_3$对应公司,$x_4$对应职级。
为了书写和计算方便,我们可以令$w_0$等于$b$,同时令$w_0$对应于特征$x_0$。由于$x_0$其实并不存在,我们可以令它的值永远为1。也就是说 $$ b=w_{0} * x_{0} , 其中 x_{0}=1 $$ 这样上面的式子就可以写成 $$ \begin{aligned} y=h(x) &=w_{1} * x_{1}+w_{2} * x_{2}+w_{3} * x_{3}+w_{4} * x_{4}+b \ &=w_{0} * x_{0}+w_{1} * x_{1}+w_{2} * x_{2}+w_{3} * x_{3}+w_{4} * x_{4} \end{aligned} $$ 我们还可以把上式写成向量的形式,(式1) $$ y=h(x)=\mathrm{w}^{T} \mathrm{x} $$ 长成这种样子模型就叫做线性模型,因为输出$y$就是输入特征$x_1, x_2, x_3,\ldots$的线性组合。
接下来,我们需要关心的是这个模型如何训练,也就是参数取什么值最合适。
机器学习有一类学习方法叫做监督学习,它是说为了训练一个模型,我们要提供这样一堆训练样本:每个训练样本既包括输入特征$x$,也包括对应的输出$y$(
另外一类学习方法叫做无监督学习,这种方法的训练样本中只有而没有。模型可以总结出特征的一些规律,但是无法知道其对应的答案。
很多时候,既有$x$又有$y$的训练样本是很少的,大部分样本都只有$x$。比如在语音到文本(STT)的识别任务中,$x$是语音,$y$是这段语音对应的文本。我们很容易获取大量的语音录音,然而把语音一段一段切分好并标注上对应文字则是非常费力气的事情。这种情况下,为了弥补带标注样本的不足,我们可以用无监督学习方法先做一些聚类,让模型总结出哪些音节是相似的,然后再用少量的带标注的训练样本,告诉模型其中一些音节对应的文字。这样模型就可以把相似的音节都对应到相应文字上,完成模型的训练。
现在,让我们只考虑监督学习。
在监督学习下,对于一个样本,我们知道它的特征$x$,以及标记$y$。同时,我们还可以根据模型$h(x)$计算得到输出$\bar{y}$。注意这里面我们用$y$表示训练样本里面的标记,也就是实际值;用带上划线的$\bar{y}$表示模型计算的出来的预测值。我们当然希望模型计算出来的$\bar{y}$和$y$越接近越好。
数学上有很多方法来表示$\bar{y}$和$y$的接近程度,比如我们可以用$\bar{y}$和$y$的差的平方的$\frac{1}{2}$来表示它们的接近程度 $$ e=\frac{1}{2}(y-\bar{y})^{2} $$ 我们把$e$叫做单个样本的误差。至于为什么前面要乘$\frac{1}{2}$,是为了后面计算方便。
训练数据中会有很多样本,比如$N$个,我们可以用训练数据中所有样本的误差的和,来表示模型的误差$E$,也就是 $$ E=e^{(1)}+e^{(2)}+e^{(3)}+\ldots+e^{(n)} $$ 上式的$e^{(1)}$表示第一个样本的误差,$e^{(2)}$表示第二个样本的误差......。
我们还可以把上面的式子写成和式的形式。(式2)
$$
\begin{aligned}
E &=e^{(1)}+e^{(2)}+e^{(3)}+\ldots+e^{(n)} \
&=\sum_{i=1}^{n} e^{(i)} \
&=\frac{1}{2} \sum_{i=1}^{n}\left(y^{(i)}-\bar{y}^{(i)}\right)^{2}
\end{aligned}
$$
其中
$$
\begin{aligned}
\bar{y}^{(i)} &=h\left(\mathrm{x}^{(i)}\right) \
&=\mathrm{w}^{T} \mathrm{x}^{(i)}
\end{aligned}
$$
(式2)中,${x}^{(i)}$表示第$i$个训练样本的特征,$y^{(i)}$表示第$i$个样本的标记,我们也可以用元组
我们当然希望对于一个训练数据集来说,误差最小越好,也就是(式2)的值越小越好。对于特定的训练数据集来说,$(x^{(i)},y^{(i)})$的值都是已知的,所以(式2)其实是参数$w$的函数。 $$ \begin{aligned} E(\mathrm{w}) &=\frac{1}{2} \sum_{i=1}^{n}\left(y^{(i)}-\bar{y}^{(i)}\right)^{2} \ &=\frac{1}{2} \sum_{i=1}^{n}\left(\mathrm{y}^{(\mathrm{i})}-\mathrm{w}^{\mathrm{T}} \mathrm{x}^{(\mathrm{i})}\right)^{2} \end{aligned} $$ 由此可见,模型的训练,实际上就是求取到合适的$w$,使(式2)取得最小值。这在数学上称作优化问题,而$E(w)$就是我们优化的目标,称之为目标函数。
大学时我们学过怎样求函数的极值。函数$y=f(x)$的极值点,就是它的导数$f^{\prime}(x)=0$的那个点。因此我们可以通过解方程$f^{\prime}(x)=0$,求得函数的极值点$\left(x_{0}, y_{0}\right)$。
不过对于计算机来说,它可不会解方程。但是它可以凭借强大的计算能力,一步一步的去把函数的极值点『试』出来。如下图所示:
首先,我们随便选择一个点开始,比如上图的$x_{0}$点。接下来,每次迭代修改$x$的为$x_{1}, x_{2}, x_{3}, \dots$,经过数次迭代后最终达到函数最小值点。
-
为啥每次修改$x$的值,都能往函数最小值那个方向前进呢?
- 原因:每次都是向函数$y=f(x)$的梯度的相反方向来修改$x$。
-
什么是梯度呢?
- 梯度是一个向量,它指向函数值上升最快的方向。
- 梯度的反方向当然就是函数值下降最快的方向了。我们每次沿着梯度相反方向去修改的值,就能走到函数的最小值附近。
- 之所以是最小值附近而不是最小值那个点,是因为我们每次移动的步长不会那么恰到好处,有可能最后一次迭代走远了越过了最小值那个点.
- 步长的选择是门手艺,如果选择小了,那么就会迭代很多轮才能走到最小值附近;如果选择大了,那可能就会越过最小值很远,收敛不到一个好的点上。
梯度下降算法的公式 $$ \mathbf{x}{\text {new}}=\mathbf{x}{\text {old}}-\eta \nabla f(x) $$ 其中,$\nabla$是梯度算子,$\nabla{f(x)}$就是指$f(x)$的梯度。$\eta$是步长,也称作学习速率。
对于上一节列出的目标函数(式2) $$ E(\mathrm{w})=\frac{1}{2} \sum_{i=1}^{n}\left(\mathrm{y}^{(\mathrm{i})}-\overline{\mathrm{y}}^{(\mathrm{i})}\right)^{2} $$ 梯度下降算法可以写成 $$ \mathrm{w}{n e w}=\mathrm{w}{o l d}-\eta \nabla E(\mathrm{w}) $$ 如果要求目标函数的最大值,那么我们就应该用梯度上升算法,它的参数修改规则是 $$ \mathbf{w}{n e w}=\mathbf{w}{o l d}+\eta \nabla E(\mathbf{w}) $$ 下面来求取$\nabla E(\mathbf{w})$,然后带入上式,就能得到线性单元的参数修改规则。
关于$\nabla E(\mathbf{w})$的推导过程,我单独把它们放到一节中。您既可以选择慢慢看,也可以选择无视。在这里,您只需要知道,经过一大串推导,目标函数$E(\mathbf{w})$的梯度是 $$ \nabla E(\mathrm{w})=-\sum_{i=1}^{n}\left(y^{(i)}-\bar{y}^{(i)}\right) \mathrm{x}^{(i)} $$ Ps: 第一次看到有点不知所措,这啥玩意,先放着
因此,线性单元的参数修改规则最后是这个样子(式3) $$ \mathrm{w}{\text {new}}=\mathrm{w}{\text {old}}+\eta \sum_{i=1}^{n}\left(y^{(i)}-\bar{y}^{(i)}\right) \mathrm{x}^{(i)} $$ 有了上面这个式子,我们就可以根据它来写出训练线性单元的代码了。
需要说明的是,如果每个样本有M个特征,则上式中的$x,w$都是M+1维向量(因为我们加上了一个恒为1的虚拟特征$x_0$,参考前面的内容),而$y$是标量。用高逼格的数学符号表示,就是 $$ \begin{aligned} &\mathbf{x}, \mathbf{w} \in \mathfrak{R}^{(M+1)}\ &y \in \mathfrak{R}^{1} \end{aligned} $$ 因为$w,x$是M+1维列向量,所以(式3)可以写成 $$ \left[\begin{array}{c} w_{0} \ w_{1} \ w_{2} \ \cdots \ w_{m} \end{array}\right]{n e w}=\left[\begin{array}{c} w{0} \ w_{1} \ w_{2} \ \cdots \ w_{m} \end{array}\right]{\text {old}}+\eta \sum{i=1}^{n}\left(y^{(i)}-\bar{y}^{(i)}\right)\left[\begin{array}{c} 1 \ x_{1}^{(i)} \ x_{2}^{(i)} \ \cdots \ x_{m}^{(i)} \end{array}\right] $$ ps: 感谢大神写这么详细的公式出来!
我们知道函数的梯度的定义就是它相对于各个变量的偏导数,所以我们写下下面的式子 $$ \begin{aligned} \nabla E(\mathrm{w}) &=\frac{\partial}{\partial \mathrm{w}} E(\mathrm{w}) \ &=\frac{\partial}{\partial \mathrm{w}} \frac{1}{2} \sum_{i=1}^{n}\left(y^{(i)}-\bar{y}^{(i)}\right)^{2} \end{aligned} $$ 我们知道和的导数等于导数的和,所以我们可以先把求和符号$\sum$里面的导数求出来,然后再把它们加在一起就行了,也就是 $$ \begin{aligned} & \frac{\partial}{\partial \mathrm{w}} \frac{1}{2} \sum_{i=1}^{n}\left(y^{(i)}-\bar{y}^{(i)}\right)^{2} \ =& \frac{1}{2} \sum_{i=1}^{n} \frac{\partial}{\partial \mathrm{w}}\left(y^{(i)}-\bar{y}^{(i)}\right)^{2} \end{aligned} $$ 现在我们可以不管高大上的$\sum$了,先专心把里面的导数求出来。 $$ \begin{aligned} & \frac{\partial}{\partial \mathrm{w}}\left(y^{(i)}-\bar{y}^{(i)}\right)^{2} \ =& \frac{\partial}{\partial \mathrm{w}}\left(y^{(i) 2}-2 \bar{y}^{(i)} y^{(i)}+\bar{y}^{(i) 2}\right) \end{aligned} $$ 我们知道,$y$是与$w$无关的常数,而$\bar{y}=w^{T} x$,下面我们根据链式求导法则来求导 $$ \frac{\partial E(\mathrm{w})}{\partial \mathrm{w}}=\frac{\partial E(\bar{y})}{\partial \bar{y}} \frac{\partial \bar{y}}{\partial \mathrm{w}} $$ 我们分别计算上式等号右边的两个偏导数 $$ \begin{aligned} \frac{\partial E(\mathrm{w})}{\partial \bar{y}} &=\frac{\partial}{\partial \bar{y}}\left(y^{(i) 2}-2 \bar{y}^{(i)} y^{(i)}+\bar{y}^{(i) 2}\right) \ &=-2 y^{(i)}+2 \bar{y}^{(i)} \ \end{aligned} $$
$$ \begin{aligned} \frac{\partial \bar{y}}{\partial \mathrm{w}} &=\frac{\partial}{\partial \mathrm{w}} \mathrm{w}^{T} \mathrm{x} \ &=\mathrm{x} \end{aligned} $$ 代入,我们求得$\sum$里面的偏导数是 $$ \begin{aligned} & \frac{\partial}{\partial \mathrm{w}}\left(y^{(i)}-\bar{y}^{(i)}\right)^{2} \ =& 2\left(-y^{(i)}+\bar{y}^{(i)}\right) \mathrm{x} \end{aligned} $$ 最后代入$\nabla E(\mathbf{w})$,求得 $$ \begin{aligned} \nabla E(\mathrm{w}) &=\frac{1}{2} \sum_{i=1}^{n} \frac{\partial}{\partial \mathrm{w}}\left(y^{(i)}-\bar{y}^{(i)}\right)^{2} \ &=\frac{1}{2} \sum_{i=1}^{n} 2\left(-y^{(i)}+\bar{y}^{(i)}\right) \mathrm{x} \ &=-\sum_{i=1}^{n}\left(y^{(i)}-\bar{y}^{(i)}\right) \mathrm{x} \end{aligned} $$ 至此,大功告成。
如果我们根据(式3)来训练模型,那么我们每次更新w的迭代,要遍历训练数据中所有的样本进行计算,我们称这种算法叫做批梯度下降(Batch Gradient Descent)。如果我们的样本非常大,比如数百万到数亿,那么计算量异常巨大。因此,实用的算法是SGD算法。在SGD算法中,每次更新w的迭代,只计算一个样本。这样对于一个具有数百万样本的训练数据,完成一次遍历就会对w更新数百万次,效率大大提升。由于样本的噪音和随机性,每次更新w并不一定按照减少$E$的方向。然而,虽然存在一定随机性,大量的更新总体上沿着$E$减少的方向前进的,因此最后也能收敛到最小值附近。下图展示了SGD和BGD的区别
如上图,椭圆表示的是函数值的等高线,椭圆中心是函数的最小值点。红色是BGD的逼近曲线,而紫色是SGD的逼近曲线。我们可以看到BGD是一直向着最低点前进的,而SGD明显躁动了许多,但总体上仍然是向最低点逼近的。
最后需要说明的是,SGD不仅仅效率高,而且随机性有时候反而是好事。今天的目标函数是一个『凸函数』,沿着梯度反方向就能找到全局唯一的最小值。然而对于非凸函数来说,存在许多局部最小值。随机性有助于我们逃离某些很糟糕的局部最小值,从而获得一个更好的模型。
完整代码请参考GitHub: https://github.com/hanbt/learn_dl/blob/master/linear_unit.py (python2.7)
同样我来用python3来实现:
因为我们已经写了感知器的代码,因此我们先比较一下感知器模型和线性单元模型,看看哪些代码能够复用。
算法 | 感知器 | 线性单元 |
---|---|---|
模型$h(x)$ |
$f(z)=\left{\begin{array}{ll}1 & z>0 \ 0 & \text { otherwise }\end{array}\right.$ |
|
训练规则 |
除了激活函数不同之外,两者的模型和训练规则是一样的(在上表中,线性单元的优化算法是SGD算法)。那么,我们只需要把感知器的激活函数进行替换即可。对于一个养成良好习惯的程序员来说,重复代码是不可忍受的。大家应该把代码保存在一个代码库中(比如git)。
from perceptron import Perceptron
#定义激活函数f
f = lambda x: x
class LinearUnit(Perceptron):
def __init__(self, input_num):
'''初始化线性单元,设置输入参数的个数'''
Perceptron.__init__(self, input_num, f)
通过继承Perceptron,我们仅用几行代码就实现了线性单元。这再次证明了面向对象编程范式的强大。
ps: 我用的是Pycharm,第一次使用from perceptron import Perceptron
是无法正常import的,原因在于路径问题,需要设置一下,很简单,右键选择文件夹,将那几个用得到的文件夹都设成(Re)souce root。
除了print() 没啥需要改的,用简单的数据进行一下测试。
def get_training_dataset():
'''
捏造5个人的收入数据
'''
# 构建训练数据
# 输入向量列表,每一项是工作年限
input_vecs = [[5], [3], [8], [1.4], [10.1]]
# 期望的输出列表,月薪,注意要与输入一一对应
labels = [5500, 2300, 7600, 1800, 11400]
return input_vecs, labels
def train_linear_unit():
'''
使用数据训练线性单元
'''
# 创建感知器,输入参数的特征数为1(工作年限)
lu = LinearUnit(1)
# 训练,迭代10轮, 学习速率为0.01
input_vecs, labels = get_training_dataset()
lu.train(input_vecs, labels, 10, 0.01)
# 返回训练好的线性单元
return lu
if __name__ == '__main__':
'''训练线性单元'''
linear_unit = train_linear_unit()
# 打印训练获得的权重
print(linear_unit)
# 测试
print('Work 3.4 years, monthly salary = %.2f' % linear_unit.predict([3.4]))
print('Work 15 years, monthly salary = %.2f' % linear_unit.predict([15]))
print('Work 1.5 years, monthly salary = %.2f' % linear_unit.predict([1.5]))
print('Work 6.3 years, monthly salary = %.2f' % linear_unit.predict([6.3]))
运行结果如下:
拟合的图像:
原文没有贴上拟合的代码,我把代码放在这里,拟合方法是网上【python图像处理】直线和曲线的拟合与绘制(curve_fit()详解)找的,不清楚大家是不是这样处理的:
import numpy as np
import matplotlib.pyplot as plt
from scipy import optimize
def f_1(x, A, B):
return A * x + B
input_vecs = [5, 3, 8, 1.4, 10.1]
labels = [5500, 2300, 7600, 1800, 11400]
plt.scatter(input_vecs[:], labels[:], 25, "red")
A1, B1 = optimize.curve_fit(f_1, input_vecs, labels)[0]
x1 = np.arange(0, 14, 1)
y1 = A1 * x1 + B1
plt.plot(x1, y1, "blue")
plt.title("curve_fit")
plt.xlabel('year')
plt.ylabel('salary')
plt.show()
ps: 总结:学会了python的包导入,面向对象代码重用,但是对于一些概念理解还需要巩固。
事实上,一个机器学习算法其实只有两部分
- 模型 从输入特征$x$预测$y$输入的那个函数$h(x)$
- 目标函数 目标函数取最小(最大)值时所对应的参数值,就是模型的参数的最优值。很多时候我们只能获得目标函数的局部最小(最大)值,因此也只能得到模型参数的局部最优值。
因此,如果你想最简洁的介绍一个算法,列出这两个函数就行了。
接下来,你会用优化算法去求取目标函数的最小(最大)值。[随机]梯度{下降|上升}算法就是一个优化算法。针对同一个目标函数,不同的优化算法会推导出不同的训练规则。我们后面还会讲其它的优化算法。
其实在机器学习中,算法往往并不是关键,真正的关键之处在于选取特征。选取特征需要我们人类对问题的深刻理解,经验、以及思考。而神经网络算法的一个优势,就在于它能够自动学习到应该提取什么特征,从而使算法不再那么依赖人类,而这也是神经网络之所以吸引人的一个方面。