前言
学习任何一种算法的最好方法都是自己手敲一遍,所以这里翻译一篇英文的深度学习入门的博文,里面很清晰地介绍了深度学习,而且实现了一个简单的三层神经网络。写在这里记录一下自己的学习过程和思考,也可以给各位读者做一个参考。
原文地址:http://www.wildml.com/2015/09/implementing-a-neural-network-from-scratch/
生成数据集
首先我们需要生成一个可以拿来玩的数据集,这里调用了 sklearn 库里面的 make_moon 函数直接生成。值得一体的是,因为 datasets 是 sklearn 的一个子库,而 python 的机制使 import sklearn 后并不会一起将所有子库一起 import 进来,所以这里需要写 from sklear import datasets 才可以正确运行。
#Generate a dataset and plot it
np.random.seed(0)
X, y = sklearn.datasets.make_moons(200, noise=0.20)
plt.scatter(X[:,0], X[:,1], s=40, c=y, cmap=plt.cm.Spectral)
这里用到的 scatter 函数是用来画散点图的,第一个参数是 x 轴,第二个参数是 y 轴,s 是 size 大小,c 是 color 颜色,而 cmap 是用来设置颜色渐变的,这里 Spectral 可以为两个标签生成不同的颜色。
最终 plt.show() 出来的图片长这个样子,这就是我们这次要用神经网络预测的数据集了。可以看到因为调用了 make_moon 函数,生成的数据集都是月亮型的。
这些数据分为两类,每个点的标签由 x 和 y 共同确定,且并不是线性可分的(不能画一条直线将两类数据分开)。不能线性可分意味着像逻辑斯蒂回归这样的线性分类器就不能用了,当然你也可以使用其他的手段改善这些线性的分类器,但这些内容不在这篇博文的讨论范围之内。
其实这也是神经网络的优势之一,即不需要人为的来做 featur engineering。隐藏层会替我们完成这一工作。
这里我们先来试一下LR的效果,这里直接调用 sklearn 库中的函数就好了。
# Train the logistic rgeression classifier
clf = sklearn.linear_model.LogisticRegressionCV()
clf.fit(X, y)
# Plot the decision boundary
plot_decision_boundary(lambda x: clf.predict(x))
最终效果可想而知。
训练一个神经网络
现在我们来构建一个三层的神经网络来完成这个分类任务。三层中有一个二维的输入层,一个二维的输出层,以及一个隐藏层。输入层的维度是2是因为每个样本是二维的;这里我们将输出层的维度也设为2,这样方便将来做其他的的拓展。对于目前的这个任务,结果输出的分别是两个标签的概率,即一个是0的概率,一个是1的概率。整个结构大概长下面整个样子。
我们可以自由选择隐藏层的维数,维数越多我们最终得到的函数就可以越复杂,但是要求的计算同样也会增加,而且最重要的是很可能会造成过拟合。那么如何选择隐藏层的大小呢?这主要取决于你当前要处理的问题,而且这其实更像是一个感性的主观认知。
另外我们还需要给隐藏层一个激活函数,从而将这层输入转化成输出。一个非线性的激活函数允许我们解决非线性的问题。常见的选择有tanh、sigmoid函数以及ReLUs,我们在这个任务中选择tanh。以上这些激活函数都有一个很重要的特征,那就是他们的求导都可以通过本身的值来计算。举例来说,tanh(x) 的求导是1-tabh2(x),所以我们计算出tanh(x)后就可以直接计算出它在此处求导的值。
因为我们希望这个神经网络能够输出概率,所以输出层的激活函数应当是归一化的。这是一种可以直接将得分转化为概率的方法。如果你很熟悉逻辑斯蒂函数,那么你可以将归一化函数看作是它在多种类别时的一种推广。
神经网络如何预测
我们的网络通过前向传递来进行预测,所谓前向传递其实就是一层层通过矩阵乘法和激活函数来传递数据。在当前的例子中,每一层参数的计算如下:
zi 是 i 层的输入,ai 是 i 层在应用激活函数之后的输出。W1、b1、W2、b2 是这个网络的参数,即是我们通过训练希望能够学到的内容。我们可以从上边的式子中看出各个参数的维度。加入我们的隐藏层有500个node,那么 W1 和 W2 就是一个2 * 500的矩阵,而 b1 则是一个一个1 * 500的矩阵,这样你就能理解为什么我说隐藏层越大我们所需的参数就越多了吧。
学习参数
所谓“学习”的过程其实就是找到一组参数(W1,b1,W2,b2)使得最终在训练集上预测的error最小。那么如何定义这个error呢?我们将其称为损失(loss),由损失函数来计算,这也是需要我们来指定的部分。对于归一化的输出结果,一个最常见的损失函数是绝对交叉熵损失(categorical cross-entropy loss ),又称负对数似然(negative log likelihood),即如果我们有 N 个训练样本,最终要分为 C 类,那么对于预测结果
y
^
\hat{y}
y^ 最终的 loss 应为
这个公式做的只是将所有训练样本的loss加在一起而已。y 和
y
^
\hat{y}
y^ 差得越远,这个 loss 越大。
然后我们通过梯度下降法来最小化这个值。梯度下降法所需要的梯度可以由后向传递算法来实现(backpropagation algorithm),而具体求梯度的过程这里就不再赘述了,最终的结果如下:
实现
现在我们来开始实现,首先是定义变量。
num_examples = len(X) # 训练集大小
nn_input_dim = 2 # 输入层维数
nn_output_dim = 2 # 输出层维数
# 梯度下降参数
epsilon = 0.01 # 学习率
reg_lambda = 0.01 # 正则化的程度
然后首先来实现损失函数的计算,我们用这个函数来衡量当前模型的有效性。
def calculate_loss(model):
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# 前向传递计算预测结果
z1 = X.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
# 计算loss,其中y中存储的是正确结果概率的index
corect_logprobs = -np.log(probs[range(num_examples), y])
data_loss = np.sum(corect_logprobs)
# Add regulatization term to loss (optional)
data_loss += reg_lambda/2 * (np.sum(np.square(W1)) + np.sum(np.square(W2)))
return 1./num_examples * data_loss
我们还要实现一个predict函数来帮我实现最后的预测过程。它通过前向传递,利用激活函数,最终返回概率较高的标签。
# 真正的预测函数 (0 or 1)
def predict(model, x):
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# Forward propagation
z1 = x.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
return np.argmax(probs, axis=1)
最后是最重要的 build_model 函数,用来训练神经网络得到参数的。
def build_model(nn_hdim, num_passes=20000, print_loss=False):
# Initialize the parameters to random values. We need to learn these.
np.random.seed(0)
W1 = np.random.randn(nn_input_dim, nn_hdim) / np.sqrt(nn_input_dim)
b1 = np.zeros((1, nn_hdim))
W2 = np.random.randn(nn_hdim, nn_output_dim) / np.sqrt(nn_hdim)
b2 = np.zeros((1, nn_output_dim))
# This is what we return at the end
model = {}
# Gradient descent. For each batch...
for i in range(0, num_passes):
# 前向传播做预测
z1 = X.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
# 逆向传播更新参数,其中y中存储的是正确结果概率的index,我们希望正确结果的概率是1,所以和1作差
delta3 = probs
delta3[range(num_examples), y] -= 1
dW2 = (a1.T).dot(delta3)
db2 = np.sum(delta3, axis=0, keepdims=True)
delta2 = delta3.dot(W2.T) * (1 - np.power(a1, 2))
dW1 = np.dot(X.T, delta2)
db1 = np.sum(delta2, axis=0)
# Add regularization terms (b1 and b2 don't have regularization terms)
dW2 += reg_lambda * W2
dW1 += reg_lambda * W1
# Gradient descent parameter update
W1 += -epsilon * dW1
b1 += -epsilon * db1
W2 += -epsilon * dW2
b2 += -epsilon * db2
# 更新model中存储的参数
model = {'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
# Optionally print the loss.
# This is expensive because it uses the whole dataset, so we don't want to do it too often.
if print_loss and i % 1000 == 0:
print('Loss after iteration %i: %f' % (i, calculate_loss(model)))
return model
当隐藏层有三个节点时
下面让我们来看看隐藏层有三个节点时的分类效果
# Build a model with a 3-dimensional hidden layer
model = build_model(3, print_loss=True)
# Plot the decision boundary
plot_decision_boundary(lambda x: predict(model, x))
plt.title( 'Decision Boundary for hidden layer size 3')
plt.show()
可以看出分类效果要比线性的逻辑斯蒂回归好多了。
调整隐藏层的大小
接下来我们让隐藏层分别取不同的值,以此来帮我们理解隐藏层节点个数的意义。
可以看出当隐藏层的节点个数较少的时候,模型可以很好地适应我们的数据;但是当节点数过多的时候,就会出现过拟合的情况。所谓过拟合,其实就是指我们希望模型做的,是找到数据背后的规律,而不是单纯的记忆数据。
这里因为当前的分类问题较为简单,所以只需要一个具有三个节点的隐藏层就够了。
结语
可以看出,所谓神经网络,其实本质还是一个优化模型。但是就是这样一个简单的模型,就已经能够解决很多问题了;而在这之上衍生出去的RNN等一系列模型,能够进一步解决更多更复杂的问题。这些更进阶的东西,就等以后有机会再继续学习。
另外,作者将所有的代码都放在Github上了,有兴趣的读者可以下载下来跑跑看:https://github.com/dennybritz/nn-from-scratch