f ( x ) = σ ( w x + b ) f(x)=\sigma(wx+b) f(x)=σ(wx+b) 该模型叫做ADLINE(Adative Linear Neuron,自适应线性神经元) ,1960年发明的
2022.4.8 仿照 张觉非的《用Python实现深度学习框架》的第二章搭建
题目训练一个模型, 给出一个人的身高、体重和体脂率,判断男女。
男性的label=1,女性label=-1,
得出的predict,predict=1判断为男性,predict=-1判断为女性
搭建难点-
树形结构,层层递归的理解
-
反向传播时,计算每个父节点对子节点的雅克比矩阵,这时矩阵求导是个难点。
比如C=step(A),求C对A的导数, C=AB, C对A对导数。
-
每次训练完毕后要把jacobi和value置为None,因为如果不置None,就不会去计算新的值
-
矩阵乘法顺序,参见机器学习中的https://www.cnblogs.com/pinard/p/10825264.html矩阵向量求导(四) 矩阵向量求导链式法则 - 刘建平Pinard - 博客园 (cnblogs.com) 。
z = f ( y ) , y = x w , ∂ z ∂ x = ∂ z ∂ y w T , ∂ z ∂ w = x T ∂ z ∂ y , ( 对 左 边 求 导 , 它 的 导 数 放 在 右 边 ; 对 右 边 求 导 , 它 的 导 数 放 在 左 边 ) z=f(y),y=xw,\ \frac{\partial z}{\partial x}=\frac{\partial z}{\partial y}w^T, \ \frac{\partial z}{\partial w}=x^T\frac{\partial z}{\partial y}, \(对左边求导,它的导数放在右边;对右边求导,它的导数放在左边) z=f(y),y=xw, ∂x∂z=∂y∂zwT,∂w∂z=xT∂y∂z,(对左边求导,它的导数放在右边;对右边求导,它的导数放在左边)
import numpy as np
from icecream import ic
import node
import ope
import loss
from graph import default_graph
def make_test():
# 生产测试数据
m_h = np.random.normal(171, 6, 500)
f_h = np.random.normal(168, 5, 500)
m_w = np.random.normal(70, 10, 500)
f_w = np.random.normal(57, 8, 500)
m_bfrs = np.random.normal(16, 2, 500)
f_bfrs = np.random.normal(22, 2, 500)
m_labels = [1] * 500
f_labels = [-1] * 500
train_set = np.array([np.concatenate((m_h, f_h)),
np.concatenate((m_w, f_w)),
np.concatenate((m_bfrs, f_bfrs)),
np.concatenate((m_labels, f_labels))
]).T
np.random.shuffle(train_set)
return train_set
if __name__=='__main__':
train_set=make_test()
x=node.Variable(shape=(3,1))
w=node.Variable(shape=(1,3))
b=node.Variable(shape=(1,1))
label=node.Variable(shape=(1,1))
w.set_value(np.mat(np.random.normal(0,0.001,(1,3))))
b.set_value(np.mat(np.random.normal(0,0.001,(1,1))))
y=ope.Add(ope.MatMul(w,x),b)
loss=loss.PerceptionLoss(ope.MatMul(label,y))
predict=ope.Step(y)
learning_rate=0.001
for epoch in range(100):
for data in train_set:
# 输入数据
x.set_value(np.mat(data[:-1]).T)
label.set_value(np.mat(data[-1]))
loss.forward()
w.backward(loss)
b.backward(loss)
# 想优化的结点都是标量结点,即都是1*n,
w.set_value(w.value - learning_rate * w.jacobi.T.reshape(w.shape()))
b.set_value(b.value - learning_rate * b.jacobi.T.reshape(b.shape()))
default_graph.clear_jacobi() #要清理每一次的雅可比矩阵,否则递归时会因为雅可比矩阵含有值而不重新计算
if epoch%10==0:
pred = []
for data in train_set:
# 输入数据
x.set_value(np.mat(data[:-1]).T)
label.set_value(np.mat(data[-1]))
predict.forward()
pred.append(predict.value[0, 0])
pred = np.array(pred) * 2 - 1
accuracy = (train_set[:, -1] == pred).astype(np.int).sum() / len(train_set)
print("训练次数为:",epoch,"时,准确率为:",accuracy)
ic(epoch,accuracy)
node.py
# node.py
from graph import default_graph
import numpy as np
from abc import ABC, abstractmethod
from icecream import ic
class Node(object):
def __init__(self,*parents):
#把结点加入默认的图
self.graph=default_graph
self.graph.add_node(self)
#性质
self.parents=list(parents)
self.children=[]
self.value=None
self.jacobi=None
for parent in self.parents:
parent.children.append(self)
@abstractmethod
def compute(self):
"""
抽象方法,根据父节点的值计算本节点的值
在前向传播时使用
"""
pass
@abstractmethod
def get_jacobi(self,parent):
# ic(type(self))
"""
抽象方法,计算本节点对某个父节点的雅可比矩阵
在反向传播时用
"""
pass
#以下为node的通用函数
def dimension(self):
assert self.value is not None
return self.value.shape[0]*self.value.shape[1]
def shape(self):
assert self.value is not None
return self.value.shape
def clear_jacobi(self):
self.jacobi=None
def reset_value(self, recursive=True):
self.value = None
if recursive:
for child in self.children:
child.reset_value()
def forward(self):
for parent in self.parents:
if parent.value is None:
parent.forward()
self.compute()
def backward(self,result):
if self.jacobi is None:
if self is result:
self.jacobi = np.mat(np.eye(self.dimension()))
else:
self.jacobi = np.mat(
np.zeros((result.dimension(), self.dimension())))
for child in self.children:
# 这里有个乘法顺序要注意,因为都是对w求导且w在左边(y=wx+b),故上个结点对该结点的导数是在右边的
if child.value is not None:
self.jacobi += child.backward(result) * child.get_jacobi(self)
return self.jacobi
class Variable(Node):
def __init__(self,shape,**kwargs):
Node.__init__(self, **kwargs)
self.size=shape
def set_value(self,value):
assert isinstance(value, np.matrix)
# 注意Variable的形状是在定义的时候就设定好的,故而使用self.size
assert self.size ==value.shape
self.reset_value()
self.value=value
ope.py
from node import Node
import numpy as np
from icecream import ic
class Operator(Node):
pass
class Add(Operator):
def compute(self):
assert self.parents is not None
self.value=np.mat(np.zeros(self.parents[0].shape()))
for parent in self.parents:
assert self.shape()==parent.shape()
self.value+=parent.value
def get_jacobi(self, parent):
return np.identity(parent.dimension())
class MatMul(Operator):
def compute(self):
assert len(self.parents)==2
assert self.parents[0].shape()[1] ==self.parents[1].shape()[0]
self.value=self.parents[0].value *self.parents[1].value
def get_jacobi(self,parent):
# 矩阵乘法的雅可比矩阵在书41页
zeros = np.mat(np.zeros((self.dimension(), parent.dimension())))
if parent is self.parents[0]:
return fill_diagonal(zeros, self.parents[1].value.T)
elif parent is self.parents[1]:
"""
原理比较复杂,见书P44,最后是通过重排列行和列的索引方式实现的
"""
jacobi = fill_diagonal(zeros, self.parents[0].value)
row_sort = np.arange(self.dimension()).reshape(self.shape()[::-1]).T.ravel()
col_sort = np.arange(parent.dimension()).reshape(parent.shape()[::-1]).T.ravel()
return jacobi[row_sort, :][:, col_sort]
class Step(Operator):
def compute(self):
assert len(self.parents)==1
self.value=np.mat(np.where(self.parents[0].value>0.0,1.0,0.0))
def get_jacobi(self,parent):
"""
因为在这个代码离,Step只用于得到预测值,故而不用对其反向传播求导就不算了
"""
pass
# 一些函数补充
def fill_diagonal(to_be_filled, filler):
"""
将 filler 矩阵填充在 to_be_filled 的对角线上
"""
assert to_be_filled.shape[0]/filler.shape[0] == to_be_filled.shape[1]/filler.shape[1]
h,w=filler.shape
n=to_be_filled.shape[0]//filler.shape[0]
for i in range(n):
to_be_filled[i*h:(i+1)*h,i*w:(i+1)*w]=filler
return to_be_filled
loss.py
from node import Node
import numpy as np
from icecream import ic
class LossFunction(Node):
pass
class PerceptionLoss(LossFunction):
"""
感知机损失,输入为正时为0,输入为负时为输入的相反数
"""
def compute(self):
x=self.parents[0].value
self.value=np.mat(np.where(x>=0.0, 0.0, -x))
def get_jacobi(self,parent):
"""
雅克比矩阵为对角阵,每个对角线元素对应一个父节点元素。
若父节点元素大于0,则
相应对角线元素(偏导数)为0,否则为-1。
"""
diag = np.where(parent.value >= 0.0, 0.0, -1)
return np.diag(diag.ravel())
graph.py
class Graph():
def __init__(self):
self.nodes=[]
def clear_jacobi(self):
for node in self.nodes:
node.clear_jacobi()
def add_node(self,node):
self.nodes.append(node)
# 全局默认计算图
default_graph = Graph()
训练结果
训练次数为: 10 时,准确率为: 0.831
训练次数为: 20 时,准确率为: 0.929
训练次数为: 30 时,准确率为: 0.912
训练次数为: 40 时,准确率为: 0.938
训练次数为: 50 时,准确率为: 0.939
训练次数为: 60 时,准确率为: 0.941
训练次数为: 70 时,准确率为: 0.942
训练次数为: 80 时,准确率为: 0.93
训练次数为: 90 时,准确率为: 0.93
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)