目录
神经元
神经元是大脑处理信息的基本单元,以细胞体为主体,由许多向周围延伸的不规则树枝状纤维构成的神经细胞,其形状很像一棵枯树的枝干。它主要由细胞体、树突、轴突和突触组成。示意图如下:
一个神经元通常具有多个树突,主要接受其他神经元传入的信号;细胞体是神经元的核心,它把各个树突传递过来的信号“加总”起来,形成一个总的刺激信号,这个总的信号会刺激与细胞体连着的轴突,当这个刺激信号超过某个强度阈值,轴突会将信号通过尾端连着的多个“突触”(图中的轴突末梢)向其他神经元传递出去。 信号的整个接收传递过程,可如下图示意:
神经元模型
可将上述神经元信号处理机理抽象成如下数学模型,如图:
(1)将信号抽象成一个n维向量,,每个分量代表信号的一个特征,图中给出的是一个3维向量;
(2)将细胞体抽象成图中的黄色方块,对信号进行线性加总,如上图,线性公式为,其中表示每个分量的权重,为截距项(或称为偏置项),为加总后的输出;
(3)经过细胞体加总的信号将作为轴突的输入,如图中黄色三角块,阈值判断函数为,比如二值函数;
也就是如果大于,输出信号z=1,否则为0,被称之为激活函数,当然激活函数可以很多,比如常见的sigmoid函数,softmax函数等,通常是非线性函数。
(4)信号z将作为输出通过突触传递给下一个神经元。
实际上,神经元的上述抽象依次对信号进行了两步处理,第一步对信号的各分量线性加权,第二步对第一步加权后的结果进行非线性处理作为输出。在图形表示中,通常将这两步仅表示为一个圆,如图中包含黄色方块和三角块的圆,后面不再做区分,这个圆的输入是,输出是。
由一个神经元构成,激活函数使用(3)中提到的阈值函数,称为感知机(Perceptron)。感知机由美国研究人员Rosenblatt在1957年提出。
神经网络模型
有了上面单个神经元的数学模型后,搭建神经网络也就顺理成章了。神经网络,顾名思义就是由一系列神经元构成的网络。但是神经网络比人的神经系统要简单的多,在神经网络中,神经元之间按层(layer)组织。每一层包含若干个神经元,层内部的神经元之间互相独立,它们之间没有信号传递。而相邻的两层之间是全连接的,即任意两个神经元都是有连接的。并且不相邻的两层神经元之间没有直接连接。神经网络的拓扑结构如图:
一般地,神经网络分为一个输入层、一个或多个隐藏层和一个输出层,如下:
(1)输入层
输入层是神经网络的数据入口,一个神经元表示数据的一个分量,也就是说如果一个数据有n个特征,则输入层有n个神经元,输入层不对数据进行处理,只负责将数据传递给后面的隐藏层;
(2)隐藏层
隐藏层的每个神经元就是前面介绍的神经元模型,它同时包含了线性和激活函数。整个神经网络的核心也在隐藏层,负责数据处理,处理后的数据送入下一个隐藏层或者输出层;
(3)输出层
输出层和隐藏层有些类似,每个神经元也要对数据进行处理,所不同的是选用的激活函数不同。对于分类问题,可选用softmax函数作为激活函数,而对于回归问题,可选用恒等函数作为激活函数。在回归中,将数据分为k类,假设为,那么输出层就有k个神经元,每个神经元对应的一个类别,假设k个神经元的输出构成一个k维向量,,需要使用softmax函数将向量作为输入,经过softmax函数计算后结果如下:
的每个分量都在,且各分量的和为1,也就是说softmax函数将一个k维向量变换成了类别的概率分布向量。也即:
下面就是比较向量的每个分量大小,值最大的分量,比如第i个分量最大,那么说明数据经过神经网络训练后的类别是(属于该类的概率最大)。上面通过softmax函数变换从概率的角度解释还算合理,实际上,最大值分量通过softmax变换后仍然是最大值分量,所以可不通过softmax函数变换,直接比较z向量的分量就可以决定分类。对于回归问题,就可以直接作为模型回归的值。
从数学角度来看,神经网络其实做了一个空间变换,假设整个神经网络的变换函数记为h,那么空间变换如下:
其中是模型的输入空间,对应模型的输出空间,即神经网络模型将n维空间变为一个k为空间,如果k<n的话,那么这就是一个降维变换。
对于神经网络,通常会以它拥有的层数来命名,但输入层不计算在内,如上图就是一个3-层神经网络。输入层也称之为0层,后面依次为第1层,第2层等等。通常使用每层的神经元个数组成的向量表示神经网络的网络结构(输入层不计算在内),比如上图网络结构为(3,2,2)。
对于图中第1层隐藏层的输出可以如下表示:
,,
展开以后如下:
。
模型参数及反向传播算法
从上面神经网络模型中可以看到,实际上模型中有三个量是我们建立模型引入的:
(1)第一个是权重向量,
(2)第二是偏置项,
(3)第三个是激活函数,通常情况下我们会事先选定某个激活函数,比如二值函数,sogmoid函数,softmax函数和relu函数等。
所以总的来说,模型的参数有两块,一块是权重向量,另一块是偏置项。神经网络模型训练也就是求解出这两个量,然后使用训练出的模型,对输入数据进行分类或者预测等。模型的训练是让“误差”最小,也就是所谓的损失函数最小,下面我们建立模型的损失函数。
假设有m个训练样本,每个训练样本有n个特征,即,是对应的标签向量,属于哪个类别,标签向量的哪个分量为1,其余为0。样本经过神经网络模型训练后的输出为,它与是同维的向量,那么数据经过训练后产生的误差其实可以使用和的距离来度量,总误差也就可以如下计算:
(1)
这就是模型的一个损失函数,为了模型参数表示方便,使用表示模型中所有权重,表示所有偏置项。模型训练其实就是求解,使得损失函数最小。
对于同一个模型,损失函数并不是唯一的,角度不同,损失函数也不同。下面我们对于分类问题再推导出一个损失函数。对于分类问题,如果数据属于类别,用表示数据所属类别,只有第j个分量为1,其余分量为0,也就是属于哪个类,的哪个分量为1,这也称之为One-Hot编码,将的最终输出记为(看成列向量),那么就得到了所属类别的概率,为了减小概率引起的精度误差,通常对概率取对数,即。那么我们希望经过神经网络训练和softmax计算后的结果向量的第j个分量的概率应该要最大(概率最大,最有可能属于该类),那么对该项添加负号后,就可以看成是数据的损失了,m个样本总损失就可以如下定义
无论选择哪个损失函数,都可以对模型进行训练,后面我们代码实现时两种方式都可以采用。下面为了方便起见,只考虑损失函数(1)式的单个数据点的损失函数,即:
,
假设第l-1层有s个神经元,第l层有q个神经元,并且都是隐藏层,s维向量是l-1层的输入,q维向量为第l层输出。神经元到第l层的权重向量矩阵为,如下:
是一个阶矩阵,其中矩阵元素表示l-1层第i个神经元到l层第j个神经元的权重。
为第l-1层到第l层偏置项组成的向量。那么由神经网路模型容易得到如下矩阵关系:
同理,第l层到第l+1层间的矩阵关系如下:
损失函数(1)式两队分别对矩阵和求导,这里用到了标量-矩阵求导和标量-向量求导,过程比较复杂,这里不再证明,以后有时间再补上,可参见机器学习之矩阵微积分及其性质,最后得到
(2)
(3)
是一个列向量,每个分量是L对分量求导,比如第i个分量为。这个向量满足如下递推关系:
(4)
表示向量或者矩阵对应元素相乘。
(4)式表明由后一层的可以递归算出前一层的。将代入(2)和(3)式就可以计算出模型参数的偏导数了。
还有一个问题需要解决。就是的初始值,它由后往前递推,初始值也就是神经网络的最后一层输出层。前面假设的l和l+1层都是隐藏层,现在假设l+1层是输出层,并且假设神经网络总共有T层(输入层不计算在内),此时有,直接得到:
右边这个是损失函数对自变量的偏导,是已知的。
我们给出一个损失函数的理解:
在输出层有,损失函数L可以理解成模型在输出层的预测误差,接着递推公式(4)将这个误差传递到了前一层的各个神经元,因为误差是引入了模型参数才引起的,最后这个误差被传递到了公式(2)和(3)表示的模型参数上。这就是误差的反向传播,称之为反向传播算法。这个算法步骤如下:
(1)给定模型参数初始值和模型训练数据,按照正向传播方向,可计算出每一层输入样本的输出值
(2)对输出层的每个神经元,计算误差;
(3)对l=T-1,T-2,...,2的各层,计算每层的误差,
根据误差,计算对应层的权重矩阵和偏置项
(4)根据梯度下降法更新权重和偏置项
然后重复步骤(1)。
公式证明
上面为了叙述方便,直接使用了递推结论,下面给出详细证明。先证明公式(2),也就是
.
先将损失函数看成是的函数,即,且,将看成是的函数。这里求导采用分子布局。如下:
是标量-矩阵求导,也就是:
根据求导链式法则,这个矩阵的第i行第j列元素满足:
注意是的第j行第i列元素,而且在中只有第j个分量中有,所以上式化为
再写出矩阵形式
如果采用分母布局就是上面的写法:
同理得到:
下面来证明(4)式,即:
.
由
写成向量形式,也就是
所以得到:
(5)
下面计算,由
得到
(注:这一步参见《机器学习之矩阵微积分及其性质》中向量-向量求导(3))
(注:这一步参见《机器学习之矩阵微积分及其性质》中向量-向量求导(7))
该式代入(5)式得到
如果采用分母布局,就得到如下结论:
TensorFlow代码实现
这一节我们产生四种类型数据,然后使用自定义神经网络模型对每种类型进行分类,四种类型数据如图:
使用神经网络训练后,分类效果如图:
从图中分类效果来看,是很不错的。各类型数据训练损失函数收敛情况如图:
四类数据损失函数的收敛速度从快到慢依次是:第一种 > 第三种 > 第四种 > 第二种。也就是通过直线分类的数据最快,同心圆类型的数据分类最慢。上面结果是通过如下结构的3-层神经网络训练分类的结果,如图:
具体代码如下:
ann_demo.py自定义一个神经网络模型,如下:
"""
自定义神经网络
"""
import numpy as np
import tensorflow as tf
class ANN(object):
def __init__(self, struct_size, log_path):
"""
ANN神经网络初始化
:param struct_size: 网络结构
:param log_path: 训练日志数据保存路径
"""
# 重置神经网络,保证神经网络多次运行
tf.reset_default_graph()
self._struct_size = struct_size
self._log_path = log_path
def define_ann(self, X):
"""
定义神经网络结构:依次定义神经网络的输入层、隐藏层、输出层和损失函数
:return:
"""
# 定义输入层,输入层大小=输入数据的特征数,输入层不对数据处理,输出等于输入
self._input = tf.placeholder(tf.float32, shape=[None, X.shape[1]], name="X")
self._labels = tf.placeholder(tf.int64, shape=[None, self._struct_size[-1]], name="Y")
pre_level_size = self._input.shape[1].value
pre_out = self._input
# 神经网络结构,每层的神经元个数,
# 比如struct=[2,3,2]表示第1层2个神经元,第2层3个神经元,第3等2个神经元
struct_size = self._struct_size
# 定义所有隐藏层(剔除输出层struct[-1])
for current_level_size in struct_size[:-1]:
# 使用正态分布初始化权重矩阵pre_level_size到current_level_size层的权重矩阵W
# 这里W的维数是(pre_level_size,current_level_size),
# 这是因为,在数学上通常使用列向量表示一个向量,而在代码实现中使用列表,也就是行向量来表示,后面均是如此
weights = tf.Variable(
tf.truncated_normal([pre_level_size, current_level_size],
stddev=1.0 / np.sqrt(float(pre_level_size))))
# 初始化偏置项向量,大小和当前隐藏层的神经元个数一致
biases = tf.Variable(tf.zeros(current_level_size))
# pre_out和biases都是行向量
pre_out = tf.nn.sigmoid(tf.matmul(pre_out, weights) + biases)
pre_level_size = current_level_size
# 定义输出层,权重矩阵和偏置项向量
weights = tf.Variable(
tf.truncated_normal([pre_level_size, struct_size[-1]],
stddev=1.0 / np.sqrt(float(pre_level_size))))
biases = tf.Variable(tf.zeros(struct_size[-1]))
# 输出层输出
self._out = tf.matmul(pre_out, weights) + biases
# 交叉熵损失函数
loss = tf.nn.softmax_cross_entropy_with_logits(labels=self._labels, logits=self._out, name='loss')
self._loss = tf.reduce_mean(loss, name='average_loss')
# 欧氏距离损失函数
# self._loss = tf.reduce_mean(tf.square(tf.to_float(self._labels) - self._out), name='average_loss')
return self
def sgd(self, X, Y, learning_rate=0.1, mini_batch_fraction=0.2, epoch=1):
"""
随机梯度下降法:
:param X: 输入样本
:param Y: 样本标签
:param learning_rate: 学习率,默认值0.1
:param mini_batch_fraction: 批量占比
:param epoch: 训练轮次
:return:
"""
# 定义随机梯度优化器
method = tf.train.GradientDescentOptimizer(learning_rate)
optimizer = method.minimize(self._loss)
# 每批次样本大小=样本数*批量占比
batch_size = int(X.shape[0] * mini_batch_fraction)
# 批次数
batch_num = int(np.ceil(1 / mini_batch_fraction))
# 创建会话并初始化
session = tf.Session()
init = tf.global_variables_initializer()
session.run(init)
# 定义日志记录
summary_writer = tf.summary.FileWriter(self._log_path, graph=tf.get_default_graph())
tf.summary.scalar("loss", self._loss)
summary = tf.summary.merge_all()
step = 0
while step < epoch:
for i in range(batch_num):
batch_x = X[i * batch_size:(i + 1) * batch_size]
batch_y = Y[i * batch_size:(i + 1) * batch_size]
session.run([optimizer], feed_dict={self._input: batch_x, self._labels: batch_y})
step += 1
# 将日志写入文件
summary_str = session.run(summary, feed_dict={self._input: X, self._labels: Y})
summary_writer.add_summary(summary_str, step)
summary_writer.flush()
self._session = session
return self
def fit(self, X, Y, learning_rate=0.3, mini_batch_fraction=0.1, epoch=500):
print("开始拟合")
self.define_ann(X)
self.sgd(X, Y, learning_rate, mini_batch_fraction, epoch)
print("结束拟合")
def predict(self, X):
"""
使用神经网络对未知数据进行预测
:param X:
:return: 概率分布
"""
session = self._session
predict = tf.nn.softmax(logits=self._out, name='predict')
prob = session.run(predict, feed_dict={self._input: X})
return prob
classification_demo.py利用自定义神经网络模型分类,如下:
"""
利用自定义神经网络分类
"""
from src.tensorflow.ann_demo import ANN
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs, make_circles, make_moons
from sklearn.preprocessing import StandardScaler, OneHotEncoder
def generate_data(samples_num):
"""
生成待分类的四种类型数据
:param samples_num: 数据点个数
:return: 四种数据类型
"""
# 固定种子,保证每次随机生成数据相同
np.random.seed(1000)
# 生成聚类类型数据点
blobs = make_blobs(n_samples=samples_num, centers=[[-2, -2], [2, 2]])
# 生成环形类型数据点
circles = make_circles(n_samples=samples_num, factor=.4, noise=.05)
# 生成月牙类型数据点,形状像月亮
moons = make_moons(n_samples=samples_num, noise=.05)
# 生成双曲类型数据点
blocks = np.random.rand(samples_num, 2) - 0.5
y = (blocks[:, 0] * blocks[:, 1] < 0) + 0
blocks = (blocks, y)
# 数据做归一化处理
scaler = StandardScaler()
blobs = (scaler.fit_transform(blobs[0]), blobs[1])
circles = (scaler.fit_transform(circles[0]), circles[1])
moons = (scaler.fit_transform(moons[0]), moons[1])
blocks = (scaler.fit_transform(blocks[0]), blocks[1])
return blobs, circles, moons, blocks
def draw_raw_data(plt, data):
"""
原始数据可视化:绘制数据散点图
:param plt:
:param data: 数据点(包含特征和类别)
:return:
"""
X, y = data
# 绘制类别1的数据散点图,圆圈表示
label1 = X[y > 0]
plt.scatter(label1[:, 0], label1[:, 1], marker="o")
# 绘制类别0的数据散点图,三角形表示
label0 = X[y == 0]
plt.scatter(label0[:, 0], label0[:, 1], marker="^", color="k")
return plt
def draw_model(plt, model):
"""
绘制分类后的模型的分离超平面:
基本思路:(1)使用100条水平线和垂直线将图形区域分割,线的交点(10000个)作为已训练模型的输入
(2)对10000个点进行预测,得到每个点属于各个类别的概率(p0,p1),取属于类别1的概率p1
(3)在平面中绘制等高线
"""
# 生成网格数据
x = np.linspace(plt.get_xlim()[0], plt.get_xlim()[1], 100)
y = np.linspace(plt.get_ylim()[0], plt.get_ylim()[1], 100)
mesh_x, mesh_y = np.meshgrid(x, y)
# ravel()数据拉平操作,np.c_列拼接操作
# np.c_[mesh_x.ravel(), mesh_y.ravel()]正好构成网格上的点
# predict对网格上的每个点计算属于各个点的概率,取出所有点属于类别1的概率p
pre_prob = model.predict(np.c_[mesh_x.ravel(), mesh_y.ravel()])[:, 1]
# 这样mesh_x和mesh_y,pre_prob就是维数相同的
pre_prob = pre_prob.reshape(mesh_x.shape)
# 绘制等高线,灰色填充概率小于等于0.5的
plt.contourf(mesh_x, mesh_y, pre_prob, levels=[0, 0.5], colors=["gray"], alpha=0.4)
return plt
def train_ann(X, y, log_path):
"""
使用ann神经网络训练模型
:param X: 输入样本
:param y: 样本标签
:param log_path: 训练过程日志数据存放路径,保存日志是为了同Tensorboard可视化数据
:return:
"""
# 对标签数据y进行One-Hot编码
enc = OneHotEncoder()
y = enc.fit_transform(y.reshape(-1, 1)).toarray()
# 定义一个(4,4,4)结构的神经网络,训练模型
model = ANN([4, 4, 2], log_path)
model.fit(X, y)
return model
def visualize(data):
"""
可视化最终的训练结果
:param data:
:return:
"""
# 创建图形框(分类前和分类后)
raw_data_figure = plt.figure(figsize=(8, 8), dpi=80)
train_model_figure = plt.figure(figsize=(8, 8), dpi=80)
# 在图形框中绘制图
for i in range(len(data)):
raw_plt = raw_data_figure.add_subplot(2, 2, i + 1)
train_plt = train_model_figure.add_subplot(2, 2, i + 1)
# 绘制分类前图形
draw_raw_data(raw_plt, data[i])
# 训练模型
X, y = data[i]
model = train_ann(X, y, "logs/data_%s" % (i + 1))
# 绘制分类后的图形
draw_raw_data(train_plt, data[i])
draw_model(train_plt, model)
raw_plt.get_xaxis().set_visible(False)
raw_plt.get_yaxis().set_visible(False)
train_plt.get_xaxis().set_visible(False)
train_plt.get_yaxis().set_visible(False)
plt.show()
if __name__ == "__main__":
data = generate_data(samples_num=200)
visualize(data)
运行classification_demo.py即可得到上面的分类结果。
最后一并感谢参考的各类文献和书籍。