小发子奥菲丝
量子比特报告|微信官方账号QbitAI
“我在网上看到过很多神经网络的实现方法,但这个是最简单明了的。”
来自普林斯顿的中国小弟Victor Zhou写的神经网络入门教程。在线代码网站Repl.it的联合创始人阿姆贾德·马萨德(Amjad Masad)看完之后评论道。
该教程发布仅几天后,就在黑客新闻论坛上获得了574个赞。程序员们纷纷称赞,这篇文章的代码写得很好,变量名很规范,让人一目了然。
让我们从零开始学习神经网络。
实现方法
构建基本模块——神经元
在讲神经网络之前,我们先讨论神经元,神经元是神经网络的基本单位。神经元先获得输入,再进行一些数学运算,然后产生一个输出。例如,一个2输入神经元的例子:
在这个神经元中,输入总共经历了三次数学运算,
前两个输入乘以权重:
x1→x1 × w1
x2→x2 × w2
将两个结果相加,并添加一个偏差:
(x1 × w1)+(x2 × w2)+ b
最后,它们被激活函数处理以得到输出:
y = f(x1 × w1 + x2 × w2 + b)
激活的作用是将无限的输入转化为可预测的输出。一种常用的激活函数是sigmoid函数:
sigmoid函数的输出介于0和1之间。我们可以理解为它将(∞,+∞)到(0,1)范围内的数压缩。正值越大,输出越接近1,负值越大,输出越接近0。
例如,上述神经元中的权重和偏移采用以下值:
w=[0,1]
b = 4
W=[0,1]用向量形式写,w1=0,w2=1。给神经元一个输入x=[2,3],神经元的输出可以向量点积的形式计算出来:
w x+b =(x1×w1)+(x2×w2)+b = 0×2+1×3+4 = 7
y=f(w⋅X+b)=f(7)=0.999
上述步骤的Python代码是:
import numpy as npdef sigmoid(x): # Our activation function: f(x) = 1 / (1 + e^(-x)) return 1 / (1 + np.exp(-x))class Neuron: def __init__(self, weights, bias): self.weights = weights self.bias = bias def feedforward(self, inputs): # Weight inputs, add bias, then use the activation function total = np.dot(self.weights, inputs) + self.bias return sigmoid(total)weights = np.array([0, 1]) # w1 = 0, w2 = 1bias = 4 # b = 4n = Neuron(weights, bias)x = np.array([2, 3]) # x1 = 2, x2 = 3print(n.feedforward(x)) # 0.9990889488055994我们在代码中调用了一个强大的Python数学函数库NumPy。
建立一个神经网络
神经网络是将一堆神经元连接在一起。下面是一个简单的神经网络示例:
这个网络有两个输入,一个隐层(h1和h2)有两个神经元,一个输出层o1有一个神经元。
隐藏层是夹在输入层和输出层之间的部分。神经网络可以有多个隐藏层。
将神经元的输入向前传递以获得输出的过程称为前馈。
假设我们上面的网络中所有神经元的权重w=[0,1]相同,偏移量b=0,激活函数为sigmoid,我们会得到什么输出?
h1=h2=f(w⋅x+b)=f((0×2)+(1×3)+0)
=f(3)
=0.9526
o1=f(w⋅[h1,h2]+b)=f((0∗h1)+(1∗h2)+0)
=f(0.9526)
=0.7216
以下是实现代码:
import numpy as np# ... code from previous section hereclass OurNeuralNetwork: ''' A neural network with: - 2 inputs - a hidden layer with 2 neurons (h1, h2) - an output layer with 1 neuron (o1) Each neuron has the same weights and bias: - w = [0, 1] - b = 0 ''' def __init__(self): weights = np.array([0, 1]) bias = 0 # The Neuron class here is from the previous section self.h1 = Neuron(weights, bias) self.h2 = Neuron(weights, bias) self.o1 = Neuron(weights, bias) def feedforward(self, x): out_h1 = self.h1.feedforward(x) out_h2 = self.h2.feedforward(x) # The inputs for o1 are the outputs from h1 and h2 out_o1 = self.o1.feedforward(np.array([out_h1, out_h2])) return out_o1network = OurNeuralNetwork()x = np.array([2, 3])print(network.feedforward(x)) # 0.7216325609518421训练神经网络
既然我们已经学会了如何建立一个神经网络,让我们来学习如何训练它。其实这是一个优化的过程。
假设有一个数据集,包含4个人的身高、体重和性别:
现在我们的目标是训练一个网络,根据某人的体重和身高来猜测他的性别。
为了简单起见,我们从每个人的身高体重中减去一个固定值,定义性别男性为1,性别女性为0。
在训练神经网络之前,我们需要一个标准来定义它的好与不好,这样我们才能对它进行改进。这就是损失。
例如,均方差(MSE)用于定义损失:
n为样本数,在上述数据集中为4;
y代表人的性别,1代表男性,0代表女性;
Ytrue是变量的真实值,ypred是变量的预测值。
顾名思义,均方误差是所有数据方差的平均值,所以我们不妨将其定义为损失函数。预测结果越好,损失越低。训练神经网络是为了尽量减少损失。
如果上述网络的输出始终为0,即预测所有人都是男性,那么损失为:
MSE= 1/4 (1+0+0+1)= 0.5
计算损失函数的代码如下:
import numpy as npdef mse_loss(y_true, y_pred): # y_true and y_pred are numpy arrays of the same length. return ((y_true - y_pred) ** 2).mean()y_true = np.array([1, 0, 0, 1])y_pred = np.array([0, 0, 0, 0])print(mse_loss(y_true, y_pred)) # 0.5减少神经网络的损失
这个神经网络还不够好,需要不断优化,把损失降到最低。我们知道改变网络的权值和偏差可以影响预测值,但是应该怎么做呢?
为了简单起见,我们减少了数据集,只包括Alice的数据。所以损失函数不考虑爱丽丝的方差:
预测值由一系列网络权重和偏移计算得出:
因此,损失函数实际上是一个具有多个权重和偏移的多元函数:
(注意!前方高能!你需要有一些多元函数微分的基础知识,比如偏导数,链导数法则。)
如果调整w1,损失函数会变大还是变小?我们需要知道偏导数L/W1是否为负才能回答这个问题。
根据链式导数法则:
并且L=(1-ypred)2,则可以获得一阶偏导数:
接下来我们要想办法得到ypred和w1的关系。我们已经知道神经元h1、h2和o1的数学运算规则:
实际上,只有神经元h1包含权重w1,所以我们再次使用链导数规则:
那就去找∂h1/∂w1.
在上面的计算中,我们遇到了sigmoid函数的导数f′(x)被激活了两次。可以容易地获得sigmoid函数的导数:
全链导数公式为:
这种反向计算偏导数的系统称为反向传播。
上面的数学符号太多了,还是把实际值带进来计算吧。H1、h2和o1
h1=f(x1⋅w1+x2⋅w2+b1)=0.0474
h2=f(w3⋅x3+w4⋅x4+b2)=0.0474
o1=f(w5⋅h1+w6⋅h2+b3)=f(0.0474+0.0474+0)=f(0.0948)=0.524
神经网络的输出y=0.524,没有强有力的证据表明它是男性(1)还是女性(0)。目前的预测效果还是很差的。
让我们再次计算当前网络的偏导数l/w1:
这个结果告诉我们,如果w1增加,损失函数L会有很小的增加。
随机梯度下降
接下来,将使用一种称为随机梯度下降(SGD)的优化算法来训练网络。
经过前面的计算,我们有了训练神经网络的所有数据。但是怎么操作呢?SGD定义了更改重量和偏移的方法:
η是一个常数,叫做学习率,它决定了我们训练网络的速度。w1减去η L/w1,就得到新的权重w1。
当l/w1为正时,w1会变小;当l/w1为负时,w1会变大。
如果用这种方法逐渐改变网络的权值W和偏移量B,损失函数会慢慢降低,从而改善我们的神经网络。
培训过程如下:
1.从数据集中选择一个样本;
2.计算所有重量和偏移的损失函数的偏导数;
3.通过使用更新公式来更新每个权重和偏移;
4.回到步骤1。
我们用Python代码实现了这个过程:
import numpy as npdef sigmoid(x): # Sigmoid activation function: f(x) = 1 / (1 + e^(-x)) return 1 / (1 + np.exp(-x))def deriv_sigmoid(x): # Derivative of sigmoid: f'(x) = f(x) * (1 - f(x)) fx = sigmoid(x) return fx * (1 - fx)def mse_loss(y_true, y_pred): # y_true and y_pred are numpy arrays of the same length. return ((y_true - y_pred) ** 2).mean()class OurNeuralNetwork: ''' A neural network with: - 2 inputs - a hidden layer with 2 neurons (h1, h2) - an output layer with 1 neuron (o1) *** DISCLAIMER ***: The code below is intended to be simple and educational, NOT optimal. Real neural net code looks nothing like this. DO NOT use this code. Instead, read/run it to understand how this specific network works. ''' def __init__(self): # Weights self.w1 = np.random.normal() self.w2 = np.random.normal() self.w3 = np.random.normal() self.w4 = np.random.normal() self.w5 = np.random.normal() self.w6 = np.random.normal() # Biases self.b1 = np.random.normal() self.b2 = np.random.normal() self.b3 = np.random.normal() def feedforward(self, x): # x is a numpy array with 2 elements. h1 = sigmoid(self.w1 * x[0] + self.w2 * x[1] + self.b1) h2 = sigmoid(self.w3 * x[0] + self.w4 * x[1] + self.b2) o1 = sigmoid(self.w5 * h1 + self.w6 * h2 + self.b3) return o1 def train(self, data, all_y_trues): ''' - data is a (n x 2) numpy array, n = # of samples in the dataset. - all_y_trues is a numpy array with n elements. Elements in all_y_trues correspond to those in data. ''' learn_rate = 0.1 epochs = 1000 # number of times to loop through the entire dataset for epoch in range(epochs): for x, y_true in zip(data, all_y_trues): # --- Do a feedforward (we'll need these values later) sum_h1 = self.w1 * x[0] + self.w2 * x[1] + self.b1 h1 = sigmoid(sum_h1) sum_h2 = self.w3 * x[0] + self.w4 * x[1] + self.b2 h2 = sigmoid(sum_h2) sum_o1 = self.w5 * h1 + self.w6 * h2 + self.b3 o1 = sigmoid(sum_o1) y_pred = o1 # --- Calculate partial derivatives. # --- Naming: d_L_d_w1 represents "partial L / partial w1" d_L_d_ypred = -2 * (y_true - y_pred) # Neuron o1 d_ypred_d_w5 = h1 * deriv_sigmoid(sum_o1) d_ypred_d_w6 = h2 * deriv_sigmoid(sum_o1) d_ypred_d_b3 = deriv_sigmoid(sum_o1) d_ypred_d_h1 = self.w5 * deriv_sigmoid(sum_o1) d_ypred_d_h2 = self.w6 * deriv_sigmoid(sum_o1) # Neuron h1 d_h1_d_w1 = x[0] * deriv_sigmoid(sum_h1) d_h1_d_w2 = x[1] * deriv_sigmoid(sum_h1) d_h1_d_b1 = deriv_sigmoid(sum_h1) # Neuron h2 d_h2_d_w3 = x[0] * deriv_sigmoid(sum_h2) d_h2_d_w4 = x[1] * deriv_sigmoid(sum_h2) d_h2_d_b2 = deriv_sigmoid(sum_h2) # --- Update weights and biases # Neuron h1 self.w1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w1 self.w2 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w2 self.b1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_b1 # Neuron h2 self.w3 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w3 self.w4 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w4 self.b2 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_b2 # Neuron o1 self.w5 -= learn_rate * d_L_d_ypred * d_ypred_d_w5 self.w6 -= learn_rate * d_L_d_ypred * d_ypred_d_w6 self.b3 -= learn_rate * d_L_d_ypred * d_ypred_d_b3 # --- Calculate total loss at the end of each epoch if epoch % 10 == 0: y_preds = np.apply_along_axis(self.feedforward, 1, data) loss = mse_loss(all_y_trues, y_preds) print("Epoch %d loss: %.3f" % (epoch, loss))# Define datasetdata = np.array([ [-2, -1], # Alice [25, 6], # Bob [17, 4], # Charlie [-15, -6], # Diana])all_y_trues = np.array([ 1, # Alice 0, # Bob 0, # Charlie 1, # Diana])# Train our neural network!network = OurNeuralNetwork()network.train(data, all_y_trues)随着学习过程的进行,损失函数逐渐减小。
现在我们可以用它来猜测每个人的性别:
# Make some predictionsemily = np.array([-7, -3]) # 128 pounds, 63 inchesfrank = np.array([20, 2]) # 155 pounds, 68 inchesprint("Emily: %.3f" % network.feedforward(emily)) # 0.951 - Fprint("Frank: %.3f" % network.feedforward(frank)) # 0.039 - M更多
本教程只是万里长征的第一步,还有很多知识要学:
1.用更大更好的机器学习库构建神经网络,比如Tensorflow,Keras,PyTorch。
2.浏览器中对神经网络的直观理解:https://playground.tensorflow.org/.
3.学习除乙状结肠以外的其他激活功能:https://keras.io/activations/
4.向SGD以外的其他优化者学习:https://keras.io/optimizers/
5.学习卷积神经网络(CNN)
6.学习递归神经网络(RNN)
这些都是维克多给自己挖的“坑”。他说以后“大概”会写这些题目,希望能把这些洞一个个补上。如果你想入门神经网络,不妨订阅他的博客。
关于这个弟弟
普林斯顿大学计算机系2019届毕业生Victor Zhou收到了大学软件工程师的聘书。他是今年8月份开始的。他做过JS编译器,也做过两个页面游戏,仇恨攻击的识别库。
最后附上我哥博客的链接:
https://victorzhou.com/
—结束—
真诚招聘
量子正在招聘编辑/记者,工作地点在北京中关村。期待有才华有热情的同学加入我们!详情请在QbitAI对话界面回复“招聘”二字。
量子qbitai头条签约作者
追踪AI技术和产品的新趋势。



