最近在搜集资料时无意中看到知乎中有人推荐此教程,特整理输出一遍。形成此系列文章。本文原文标题为:
《零基础入门深度学习(1) - 感知器》
原理部分请直接参考原文:目前只分析源码:此代码修改原码,为python3环境。
from functools import reduce
class Perceptron(object):
def __init__(self, input_num, activator):
"""
初始化感知器,设置输入参数的个数,以及激活函数。
激活函数的类型为double -> double。
"""
self.activator = activator
# 权重向量初始化为0。
self.weights = [0.0 for _ in range(input_num)]
# 偏置项初始化为0
self.bias = 0.0
def __str__(self):
"""
打印学习到的权重、偏置项
"""
return "weights\t:%s\nbias\t:%f\n" % (self.weights, self.bias)
def predict(self, input_vec):
"""
输入向量,输出感知器的计算结果。
"""
# 把input_vec[x1, x2, x3, ...]和weights[w1, w2, w3, ...]打包在一起。
# 注释:原文当中zip放在python3环境中,不能跑通。
# python3中zip函数返回的值类型为zip。惰性迭代了。无法满足map的需求。
# 变成[(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)
# return self.activator(reduce(lambda a, b: a+b, map(lambda (x, w) : x * w, zip(input_vec, self.weights)), 0.0) + self.bias)
def train(self, input_vecs, labels, iteration, rate):
"""
输入训练数据:一组向量、与每个向量对应的label;以及训练轮数、学习率
"""
for i in range(iteration):
print("第 %s 轮训练:" % (i+1))
self._one_iteration(input_vecs, labels, rate)
def _one_iteration(self, input_vecs, labels, rate):
"""
一次迭代,把所有的训练数据过一遍
"""
# 把输入和输出打包在一起,成为样本的列表[(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))
# self.weights = list(map(lambda (x, w): w + rate * delta * x, zip(input_vec, self.weights)))
# 此处与原文也不相同,原因是python3中map函数返回的是一个map类型的序列,而不是列表了。类似于zip。
# print(type(self.weights))
# 更新bias
self.bias += rate * delta
print(delta, self.weights, self.bias) # 观察delta的值,和weight和bias的值是否按照规则更新。
# 接下来,我们利用这个感知器类去实现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_percetron():
"""
使用and真值表训练感知器。
"""
# 创建感知器,输入参数个数为2,因为and的是二元函数。激活函数为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_perceptron = train_and_percetron()
# 打印训练获得的权重
print(and_perceptron)
# 测试
print("1 and 1 = %d" % and_perceptron.predict([1, 1]))
print("0 and 0 = %d" % and_perceptron.predict([0, 0]))
print("1 and 0 = %d" % and_perceptron.predict([1, 0]))
print("0 and 1 = %d" % and_perceptron.predict([0, 1]))
输出:
weights :[0.1, 0.2]
bias :-0.200000
1 and 1 = 1
0 and 0 = 0
1 and 0 = 0
0 and 1 = 0
第一步:
从背景开始分析:输入层维度:数据的特征维度,因为是and是二元函数,二维。所以输入层x维度为2。或者说两个神经元。那么相应的系数w也应该是两个。系数也是二维。这样就有了上面代码中:
第12行:self.weights = [0.0 for _ in range(input_num)]。w和x都有了,偏置b为共享,所以只有一个,所有参数初始设置为0.0。
第二步:
拟合的本质就是wx+b=y,然后真实标签值t与预测值y的差值来调节w和b的值。使y和t努力接近。带入训练数据每次更新模型参数。模型参数调整策略:
因此公式对应63行的:w + rate * delta * x和67行:self.bias += rate * delta。其中61行中:delta = label - output。即t-y。rate为学习率。
第三步:
每个样本依次输入到模型(将x与w和b运算,通过激活函数输出)然后每一次都会执行预测predict函数,(第一次通过初始化参数的w和b计算,后续数据通过上面规则(_update_weights函数)调整的w和b来计算)。不断循环,直到迭代次数结束。所以每一次的_one_iteration函数执行两步:第一步:predict,第二步:根据t-y来更新w和b的_update_weights函数。因为没有考察的目标函数,所以不会有计算loss之类的操作。
这个循环执行的过程放在了train函数中执行。通过for 将iteration次数的迭代用完。
第一个问题需要反思:模型到底是什么?
第一运算规则:第二规则中的计算用到的系数。
运算规则就是用w和输入x相乘加b,然后激活输出,定义了模型的操作结构,后续一般称为网络结构:到底是CNN还是RNN。同时网络结构又细分LeNet和resNet等等。。都是运算规则。
运算中用到的计算,这些系数,就是模型的参数。
例子:最简单的说拟合直线:y = kx + b。规则是直线,所以我们定义一维直线公式。此公式中有两个参数需要计算k和b。
第二个问题需要反思:模型在哪里?这么抽象,code中在哪里?
网络模型:一般有模型网络结构的会单独建立一个文件,告诉我们运算规则。网络有多少层,每一层有哪些操作。数据输入后要怎么进入网络模型。
参数模型:计算的参数在哪里?本例中,我们仔细思考,其实定义类的时候,在__init__中,将参数以类属性的形式保存下来,在内存中开辟了空间。等着我们运算和使用。最后通过不断更新,这些对象的属性值发生变化。
总之:参数一直保存在类实例化之后对象的属性值中。这个值导出来,就是模型参数文件。训练实际上是更新对象的属性值w和b。
第三个问题:在哪里结合?模型怎么用的。
在predict中结合。predict中将x的值,带入模型,通过与参数结合按照模型运算规则进行运算。
这个predict即在训练时应用运算规则。也要在模型最终确定后用来工作,即测试集测试结果。第二段代码中:38到41。做的事情,也是我们真正使用模型的操作。
代码中还有一点值得借鉴:连输入都用函数来执行,是分的真利索。 真clean code。
本文最后:引用原文中一段经典话:
值得高兴的是,你终于已经走出了深度学习入门的第一步,这是巨大的进步;坏消息是,这仅仅是最简单的部分,后面还有无数艰难险阻等着你。不过,你学的困难往往意味着别人学的也困难,掌握一门高门槛的技艺,进可糊口退可装逼,是很值得的。