(因为我不是专家,这里的解释只用于理解人工神经元模拟了生物神经元的什么地方,不一定完全准确)
神经元主要由细胞体和细胞突组成,而细胞突分为树突 (Dendrites) 和轴突 (Axon),树突负责接收其他神经元输入的电流,而轴突负责把电流输出给其他神经元。一个神经元可以通过树突从多个神经元接收电流,如果电流没有达到某个阈值则神经元不会把电流输出,如果电流达到了某个阈值则神经元会通过轴突的突触把电流输出给其他神经元,这样的规则被称为全有全无律。输入电流达到阈值以后输出电流的状态又称为到达动作电位,动作电位会持续 1 ~ 2 毫秒,之后会进入约 0.5 毫秒的绝对不应期,无论输入多大的电流都不会输出,然后再进入约 3.5 毫秒的相对不应期,需要电流达到更大的阈值才会输出,最后返回静息电位。神经元之间连接起来的网络称为神经元网络,人的大脑中大约有 860 亿个神经元,因为 860 亿个神经元可以同时工作,所以目前的计算机无法模拟这种工作方式 (除非开发专用的芯片),只能模拟一部分的工作方式和使用更小规模的网络。
计算机模拟神经元网络使用的是人工神经元,单个人工神经元可以用以下公式表达:
其中 n 代表输入的个数,你可以把 n 看作这个神经元拥有的树突个数,x 看作每个树突输入电流的值;而 w (weight) 代表各个输入的权重,也就是各个树突对电流大小的调整;而 b (bias) 用于调整各个输入乘权重相加后的值,使得这个值可以配合某个阈值工作;而 g 则是激活函数,用于判断值是否达到阈值并输出和输出多少,通常会使用非线性函数;而 y 则是输出的值,可以把它看作轴突输出的电流,连接这个 y 到其他神经元就可以组建神经元网络。
我们在前两篇看到的其实就是只有一个输入并且没有激活函数的单个人工神经元,把同样的输入传给多个神经元 (第一层),然后再传给其他神经元 (第二层),然后再传给其他神经元 (第三层) 就可以组建人工神经元网络了,同一层的神经元个数越多,神经元的层数越多,网络就越强大,但需要更多的运算时间并且更有可能发生第一篇文章讲过的过拟合 (Overfitting) 现象。
下图是人工神经元网络的例子,有 3 输入 1 个输出,经过 3 层处理,第 1 层和第 2 层各有两个神经元对应隐藏值 (中间值),第 3 层有一个神经元对应输出值:
神经元中包含的 w 和 b 就是我们需要通过机器学习调整的参数值。
如果你觉得图片有点难以理解,可以看转换后的代码:
h11 = g(x1 * w111 + x2 * w112 + x3 * w113 + b11)
h12 = g(x1 * w121 + x2 * w122 + x3 * w123 + b12)
h21 = g(h11 * w211 + h12 * w212 + b21)
h22 = g(h11 * w221 + h12 * w222 + b22)
y = g(h21 * w311 + h22 * w312 + b31)
很多痴迷人工神经元网络的学者声称人工神经元网络可以模拟人脑的工作方式,做到某些领域上超过人脑的判断,但实际上这还有很大的争议,我们可以看到人工神经元的连接方式只会按固定的模式,判断是否达到阈值并输出的逻辑也无法做到和生物神经元一样(目前还没有解明),并且也没有生物神经元的不应期,所以也有学者声称人工神经元不过只是做了复杂的数学运算来模拟逻辑判断,需要根据不同的场景切换不同的计算方法,使用这种方式并不能达到人脑的水平。
在前一篇文章我们已经稍微了解过机器学习框架 pytorch,现在我们来看看怎么使用 pytorch 封装的线性模型,以下代码运行在 python 的 REPL 中:
# 导入 pytorch 类库
>>> import torch
# 创建 pytorch 封装的线性模型,设置输入有 3 个输出有 1 个
>>> model = torch.nn.Linear(in_features=3, out_features=1)
# 查看线性模型内部包含的参数列表
# 这里一共包含两个参数,第一个参数是 1 行 3 列的矩阵分别表示 3 个输入对应的 w 值 (权重),第二个参数表示 b 值 (偏移)
# 初始值会随机生成 (使用 kaiming_uniform 生成正态分布)
>>> list(model.parameters())
[Parameter containing:
tensor([[0.0599, 0.1324, 0.0099]], requires_grad=True), Parameter containing:
tensor([-0.2772], requires_grad=True)]
# 定义输入和输出
>>> x = torch.tensor([1, 2, 3], dtype=torch.float)
>>> y = torch.tensor([6], dtype=torch.float)
# 把输入传给模型
>>> p = model(x)
# 查看预测输出值
# 1 * 0.0599 + 2 * 0.1324 + 3 * 0.0099 - 0.2772 = 0.0772
>>> p
tensor([0.0772], grad_fn=<AddBackward0>)
# 计算误差并自动微分
>>> l = (p - y).abs()
>>> l
tensor([5.9228], grad_fn=<AbsBackward>)
>>> l.backward()
# 查看各个参数对应的导函数值
>>> list(model.parameters())[0].grad
tensor([[-1., -2., -3.]])
>>> list(model.parameters())[1].grad
tensor([-1.])
以上可以看作 1 层 1 个神经元,很好理解吧?我们来看看 1 层 2 个神经元:
# 导入 pytorch 类库
>>> import torch
# 创建 pytorch 封装的线性模型,设置输入有 3 个输出有 2 个
>>> model = torch.nn.Linear(in_features=3, out_features=2)
# 查看线性模型内部包含的参数列表
# 这里一共包含两个参数
# 第一个参数是 2 行 3 列的矩阵分别表示 2 个输出和 3 个输入对应的 w 值 (权重)
# 第二个参数表示 2 个输出对应的 b 值 (偏移)
>>> list(model.parameters())
[Parameter containing:
tensor([[0.1393, 0.5165, 0.2910],
[0.2276, 0.1579, 0.1958]], requires_grad=True), Parameter containing:
tensor([0.2566, 0.1701], requires_grad=True)]
# 定义输入和输出
>>> x = torch.tensor([1, 2, 3], dtype=torch.float)
>>> y = torch.tensor([6, -6], dtype=torch.float)
# 把输入传给模型
>>> p = model(x)
# 查看预测输出值
# 1 * 0.1393 + 2 * 0.5165 + 3 * 0.2910 + 0.2566 = 2.3019
# 1 * 0.2276 + 2 * 0.1579 + 3 * 0.1958 + 0.1701 = 1.3009
>>> p
tensor([2.3019, 1.3009], grad_fn=<AddBackward0>)
# 计算误差并自动微分
# (abs(2.3019 - 6) + abs(1.3009 - -6)) / 2 = 5.4995
>>> l = (p - y).abs().mean()
>>> l
tensor(5.4995, grad_fn=<MeanBackward0>)
>>> l.backward()
# 查看各个参数对应的导函数值
# 因为误差取了 2 个值的平均,所以求导函数值的时候会除以 2
>>> list(model.parameters())[0].grad
tensor([[-0.5000, -1.0000, -1.5000],
[ 0.5000, 1.0000, 1.5000]])
>>> list(model.parameters())[1].grad
tensor([-0.5000, 0.5000])
现在我们来试试用线性模型来学习符合 x_1 * 1 + x_2 * 2 + x_3 * 3 + 8 = y
的数据,输入和输出会使用矩阵定义:
# 引用 pytorch
import torch
# 给随机数生成器分配一个初始值,使得每次运行都可以生成相同的随机数
# 这是为了让训练过程可重现,你也可以选择不这样做
torch.random.manual_seed(0)
# 创建线性模型,设置有 3 个输入 1 个输出
model = torch.nn.Linear(in_features=3, out_features=1)
# 创建损失计算器
loss_function = torch.nn.MSELoss()
# 创建参数调整器
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 随机生成原始数据集,一共 20 组数据,每条数据有 3 个输入
dataset_x = torch.randn((20, 3))
dataset_y = dataset_x.mm(torch.tensor([[1], [2], [3]], dtype=torch.float)) + 8
print(f"dataset_x: {dataset_x}")
print(f"dataset_y: {dataset_y}")
# 切分训练集 (12 组),验证集 (4 组) 和测试集 (4 组)
random_indices = torch.randperm(dataset_x.shape[0])
traning_indices = random_indices[:int(len(random_indices)*0.6)]
validating_indices = random_indices[int(len(random_indices)*0.6):int(len(random_indices)*0.8):]
testing_indices = random_indices[int(len(random_indices)*0.8):]
traning_set_x = dataset_x[traning_indices]
traning_set_y = dataset_y[traning_indices]
validating_set_x = dataset_x[validating_indices]
validating_set_y = dataset_y[validating_indices]
testing_set_x = dataset_x[testing_indices]
testing_set_y = dataset_y[testing_indices]
# 开始训练过程
for epoch in range(1, 10000):
print(f"epoch: {epoch}")
# 根据训练集训练并修改参数
# 切换模型到训练模式,将会启用自动微分,批次正规化 (BatchNorm) 与 Dropout
model.train()
# 计算预测值
# 20 行 3 列的矩阵乘以 3 行 1 列的矩阵 (由 weight 转置得到) 等于 20 行 1 列的矩阵
predicted = model(traning_set_x)
# 计算损失
loss = loss_function(predicted, traning_set_y)
# 打印除错信息
print(f"loss: {loss}, weight: {model.weight}, bias: {model.bias}")
# 从损失自动微分求导函数值
loss.backward()
# 使用参数调整器调整参数
optimizer.step()
# 清空导函数值
optimizer.zero_grad()
# 检查验证集
# 切换模型到验证模式,将会禁用自动微分,批次正规化 (BatchNorm) 与 Dropout
model.eval()
predicted = model(validating_set_x)
validating_accuracy = 1 - ((validating_set_y - predicted).abs() / validating_set_y).abs().mean()
print(f"validating x: {validating_set_x}, y: {validating_set_y}, predicted: {predicted}")
# 如果验证集正确率大于 99 %,则停止训练
print(f"validating accuracy: {validating_accuracy}")
if validating_accuracy > 0.99:
break
# 检查测试集
predicted = model(testing_set_x)
testing_accuracy = 1 - ((testing_set_y - predicted).abs() / testing_set_y).abs().mean()
print(f"testing x: {testing_set_x}, y: {testing_set_y}, predicted: {predicted}")
print(f"testing accuracy: {testing_accuracy}")
输出结果如下:
dataset_x: tensor([[ 0.8487, 0.6920, -0.3160],
[-2.1152, -0.3561, 0.4372],
[ 0.4913, -0.2041, 0.1198],
[ 1.2377, 1.1168, -0.2473],
[-1.0438, -1.3453, 0.7854],
[ 0.9928, 0.5988, -1.5551],
[-0.3414, 1.8530, 0.4681],
[-0.1577, 1.4437, 0.2660],
[ 1.3894, 1.5863, 0.9463],
[-0.8437, 0.9318, 1.2590],
[ 2.0050, 0.0537, 0.4397],
[ 0.1124, 0.6408, 0.4412],
[-0.2159, -0.7425, 0.5627],
[ 0.2596, 0.5229, 2.3022],
[-1.4689, -1.5867, -0.5692],
[ 0.9200, 1.1108, 1.2899],
[-1.4782, 2.5672, -0.4731],
[ 0.3356, -1.6293, -0.5497],
[-0.4798, -0.4997, -1.0670],
[ 1.1149, -0.1407, 0.8058]])
dataset_y: tensor([[ 9.2847],
[ 6.4842],
[ 8.4426],
[10.7294],
[ 6.6217],
[ 5.5252],
[12.7689],
[11.5278],
[15.4009],
[12.7970],
[11.4315],
[10.7175],
[ 7.9872],
[16.2120],
[ 1.6500],
[15.0112],
[10.2369],
[ 3.4277],
[ 3.3199],
[11.2509]])
epoch: 1
loss: 142.77590942382812, weight: Parameter containing:
tensor([[-0.0043, 0.3097, -0.4752]], requires_grad=True), bias: Parameter containing:
tensor([-0.4249], requires_grad=True)
validating x: tensor([[-0.4798, -0.4997, -1.0670],
[ 0.8487, 0.6920, -0.3160],
[ 0.1124, 0.6408, 0.4412],
[-1.0438, -1.3453, 0.7854]]), y: tensor([[ 3.3199],
[ 9.2847],
[10.7175],
[ 6.6217]]), predicted: tensor([[-0.1385],
[ 0.3020],
[-0.0126],
[-1.1801]], grad_fn=<AddmmBackward>)
validating accuracy: -0.04714548587799072
epoch: 2
loss: 131.40403747558594, weight: Parameter containing:
tensor([[ 0.0675, 0.4937, -0.3163]], requires_grad=True), bias: Parameter containing:
tensor([-0.1970], requires_grad=True)
validating x: tensor([[-0.4798, -0.4997, -1.0670],
[ 0.8487, 0.6920, -0.3160],
[ 0.1124, 0.6408, 0.4412],
[-1.0438, -1.3453, 0.7854]]), y: tensor([[ 3.3199],
[ 9.2847],
[10.7175],
[ 6.6217]]), predicted: tensor([[-0.2023],
[ 0.6518],
[ 0.3935],
[-1.1479]], grad_fn=<AddmmBackward>)
validating accuracy: -0.03184401988983154
epoch: 3
loss: 120.98343658447266, weight: Parameter containing:
tensor([[ 0.1357, 0.6687, -0.1639]], requires_grad=True), bias: Parameter containing:
tensor([0.0221], requires_grad=True)
validating x: tensor([[-0.4798, -0.4997, -1.0670],
[ 0.8487, 0.6920, -0.3160],
[ 0.1124, 0.6408, 0.4412],
[-1.0438, -1.3453, 0.7854]]), y: tensor([[ 3.3199],
[ 9.2847],
[10.7175],
[ 6.6217]]), predicted: tensor([[-0.2622],
[ 0.9860],
[ 0.7824],
[-1.1138]], grad_fn=<AddmmBackward>)
validating accuracy: -0.016991496086120605
省略途中输出
epoch: 637
loss: 0.001102567883208394, weight: Parameter containing:
tensor([[1.0044, 2.0283, 3.0183]], requires_grad=True), bias: Parameter containing:
tensor([7.9550], requires_grad=True)
validating x: tensor([[-0.4798, -0.4997, -1.0670],
[ 0.8487, 0.6920, -0.3160],
[ 0.1124, 0.6408, 0.4412],
[-1.0438, -1.3453, 0.7854]]), y: tensor([[ 3.3199],
[ 9.2847],
[10.7175],
[ 6.6217]]), predicted: tensor([[ 3.2395],
[ 9.2574],
[10.6993],
[ 6.5488]], grad_fn=<AddmmBackward>)
validating accuracy: 0.9900396466255188
testing x: tensor([[-0.3414, 1.8530, 0.4681],
[-1.4689, -1.5867, -0.5692],
[ 1.1149, -0.1407, 0.8058],
[ 0.3356, -1.6293, -0.5497]]), y: tensor([[12.7689],
[ 1.6500],
[11.2509],
[ 3.4277]]), predicted: tensor([[12.7834],
[ 1.5438],
[11.2217],
[ 3.3285]], grad_fn=<AddmmBackward>)
testing accuracy: 0.9757462739944458
可以看到最终 weight 接近 1, 2, 3,bias 接近 8。和前一篇文章最后的例子比较还可以发现代码除了定义模型的部分以外几乎一模一样 (后面的代码基本上都是相同的结构,这个系列是先学套路在学细节😈) 。
看到这里你可能会觉得,怎么我们一直都在学习一次方程式,不能做更复杂的事情吗😡?
如我们看到的,线性模型只能计算一次方程式,如果我们给的数据不满足任何一次方程式,这个模型将无法学习成功,那么叠加多层线性模型可以学习更复杂的数据吗?
以下是一层和两层人工神经元网络的公式例子:
因为我们还没有学到激活函数,先去掉激活函数,然后展开没有激活函数的两层人工神经元网络看看是什么样子:
从上图可以看出,如果没有激活函数,两层人工神经元网络和一层神经元网络效果是一样的,实际上,如果没有激活函数不管叠加多少层都和一层一样🙀,所以如果我们要构建多层网络,必须添加激活函数。
激活函数的作用是让人工神经元网络支持学习非线性的数据,所谓非线性的数据就是不满足任何一次方程式的数据,输出和输入之间不会按一定比例变化。举个很简单的例子,如果需要按码农的数量计算某个项目所需的完工时间,一个码农需要一个月,两个码农需要半个月,三个码农需要十天,四个码农需要一个星期,之后无论请多少个码农都需要一个星期,多出来的码农只会吃闲饭,使用图表可以表现如下:
如果不用激活函数,只用线性模型可以调整 w 到 -5.4,b 到 30,使用图表对比 -5.4 x + 30
和实际数据如下,我们可以看到不仅预测的误差较大,随着码农数量的增长预测所需的完工时间会变为负数😱:
那么使用激活函数会怎样呢?我们以最简单也是最流行的激活函数 ReLU (Rectified Linear Unit) 为例,ReLU 函数的定义如下:
意思是传入值大于 0 时返回原值,否则返回 0,用 python 代码可以表现如下:
def relu(x):
if x > 0:
return x
return 0
再看看以下结合了 ReLU 激活函数的两层人工神经元网络 (最后一层不使用激活函数):
试试计算上面的例子:
def relu(x):
if x > 0:
return x
return 0
for x in range(1, 11):
h1 = relu(x * -1 + 2)
h2 = relu(x * -1 + 3)
h3 = relu(x * -1 + 4)
y = h1 * 10 + h2 * 2 + h3 * 3 + 7
print(x, y)
输入如下,可以看到添加激活函数后模型能够支持计算上面的非线性数据😎:
1 30
2 15
3 10
4 7
5 7
6 7
7 7
8 7
9 7
10 7
激活函数除了上述介绍的 ReLU 还有很多,以下是一些常用的激活函数:
如果你想看它们的曲线和导函数可以参考以下链接:
添加激活函数以后求导函数值同样可以使用连锁律,如果第一层返回的是正数,那么就和没有 ReLU 时的计算一样 (导函数为 1):
>>> w1 = torch.tensor(5.0, requires_grad=True)
>>> b1 = torch.tensor(1.0, requires_grad=True)
>>> w2 = torch.tensor(6.0, requires_grad=True)
>>> b2 = torch.tensor(7.0, requires_grad=True)
>>> x = torch.tensor(2.0)
>>> y = torch.nn.functional.relu(x * w1 + b1) * w2 + b2
# 假设我们要调整 w1, b1, w2, b2 使得 y 接近 0
>>> y
tensor(73., grad_fn=<AddBackward0>)
>>> y.abs().backward()
# w1 的导函数值为 x * w2
>>> w1.grad
tensor(12.)
# b1 的导函数值为 w2
>>> b1.grad
tensor(6.)
# w2 的导函数值为 x * w1 + b1
>>> w2.grad
tensor(11.)
# b1 的导函数值为 1
>>> b2.grad
tensor(1.)
假设第一层返回的是负数,那么 ReLU 会让上一层的导函数值为 0 (导函数为 0):
>>> w1 = torch.tensor(5.0, requires_grad=True)
>>> b1 = torch.tensor(1.0, requires_grad=True)
>>> w2 = torch.tensor(6.0, requires_grad=True)
>>> b2 = torch.tensor(7.0, requires_grad=True)
>>> x = torch.tensor(-2.0)
>>> y = torch.nn.functional.relu(x * w1 + b1) * w2 + b2
>>> y
tensor(7., grad_fn=<AddBackward0>)
>>> y.backward()
>>> w1.grad
tensor(-0.)
>>> b1.grad
tensor(0.)
>>> w2.grad
tensor(0.)
>>> b2.grad
tensor(1.)
虽然 ReLU 可以适合大部分场景,但有时候我们还是需要选择其他激活函数,选择激活函数一般会考虑以下的因素:
从上图我们可以看到 ReLU 的计算量是最少的,Sigmoid 和 Tanh 的计算量则很大,使用计算量大的激活函数会导致学习过程更慢。
而梯度消失 (Vanishing Gradient) 问题则是在人工神经元网络层数增多以后出现的问题,如果层数不断增多,使用连锁律求导函数的时候会不断叠加激活函数的导函数,部分激活函数例如 Sigmoid 和 Tanh 的导函数会随着叠加次数增多而不断的减少导函数值。例如有 3 层的时候,第 3 层导函数值可能是 6, -2, 1
,第 2 层的导函数值可能是 0.07, 0.68, -0.002
,第 1 层的导函数值可能是 0.0004, -0.00016, -0.00003
,也就是前面的层参数基本上不会调整,只有后面的层参数不断变化,导致浪费计算资源和不能完全发挥模型的能力。激活函数 ReLU 则不会存在梯度消失问题,因为不管叠加多少层只要中间不存在负数则导函数值会一直传递上去,这也是 ReLU 流行的原因之一。
停止学习问题是模型达到某个状态 (未学习成功) 以后不管怎么调整参数都不会变化的问题,一个简单的例子是使用 ReLU 时,如果第一层的输出刚好全部都是负数,那隐藏值则全部为 0,导函数值也为 0,不管再怎么训练参数都不会变化。LeakyReLU 与 ELU 则是为了解决停止学习问题产生的,但因为增加计算量和允许负数可能会带来其他影响,我们一般都会先使用 ReLU,出现停止学习问题再试试 ReLU 的派生函数。
Sigmoid 和 Tanh 虽然有梯度消失问题,但是它们可以用于在指定场景下转换数值到 0 ~ 1 和 -1 ~ 1。例如 Sigmoid 可以用在最后一层表现可能性,100 表示非常有可能 (转换到 1),50 也代表非常有可能 (转换到 1),1 代表比较有可能 (转换到 0.7311),0 代表不确定 (转换到 0.5),-1 代表比较不可能 (转换到 0.2689),-100 代表很不可能 (转换到 0)。而后面文章介绍的 LSTM 模型也会使用 Sigmoid 决定需要忘记哪些内部状态,Tanh 决定应该怎样更新内部状态。此外还有 Softmax 等一般只用在最后一层的函数,Softmax 可以用于在分类的时候判断哪个类别可能性最大,例如识别猫狗猪的时候最后一层给出 6, 5, 8
,数值越大代表属于该分类的可能性越高,经过 Softmax 转换以后就是 0.1142, 0.0420, 0.8438
,代表有 11.42% 的可能性是猫,4.2% 的可能性是狗,84.38% 的可能性是猪。
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
加入交流群
请使用微信扫一扫!