第二周:神经网络的编程基础(Basics of Neural Network programming)
二分类(Binary Classification)
二分类,给定图片,如果是猫,返回1,否则返回0
图片 64 x 64,先加载成一个 64 x 64 x 3 的矩阵,然后再平铺成一个向量 length = 12288,方便计算。
定义要用到的符号
逻辑回归(Logistic Regression)
Give \(x\), want \(\hat{y}=P(y=1|x)\), \(x \in \mathbb{R}^{n_x}, 0 \leq \hat{y} \leq 1\)
Parameter : \(w \in \mathbb{R}^{n_x}, \;b \in \mathbb{R}\)
Output : \(\hat{y} = \sigma(w^{\top}x + b)\), \(\;\; z = w^{\top}x + b\), \(\;\;\sigma(z)=\frac{1}{1+e^{-z}}\)
if \(z\) large \(\sigma(z) \approx \frac{1}{1+0} = 1\)
if \(z\) large negative number \(\sigma(z) \approx \frac{1}{1+ \text{BigNum}} = 0\)
逻辑回归的代价函数(Logistic Regression Cost Function)
\(\hat{y}^{(i)}=\sigma(w^{\top}x^{(i)} + b)\;\), where \(\sigma(z^{(i)})=\frac{1}{1+e^{-z^{(i)}}}\)
Given {\((x^{(1)},y^{(1)}),...,(x^{(m)}, y^{(m)})\)}, want \(\hat{y}^{(i)}\approx y^{(i)}\)
Loss(error) function:
\(\mathcal{L}(\hat{y}, y)=-\Big(y \; \mathrm{log} \; \hat{y}+(1-y) \; \mathrm{log} \; (1-\hat{y})\Big)\)
损失函数(loss function)是在单个训练样本中定义的,它衡量了在单个训练样本上的表现。
下面我要定义一个代价函数(cost function),它衡量的是在全体样本上的表现。
\(J(w, b) = \frac{1}{m}\sum\limits_{i=1}^{m}\mathcal{L}(\hat{y}^{(i)}, y^{(i)})=-\frac{1}{m}\sum\limits_{i=1}^{m}\Big[y^{(i)} \; \mathrm{log} \; \hat{y}^{(i)}+(1-y^{(i)}) \; \mathrm{log} \; (1-\hat{y}^{(i)})\Big]\)
梯度下降(Gradient Descent)
Recap:
\(\hat{y}^{(i)}=\sigma(w^{\top}x^{(i)} + b)\;\), \(\;\sigma(z)=\frac{1}{1+e^{-z}}\)
\(J(w, b) = \frac{1}{m}\sum\limits_{i=1}^{m}\mathcal{L}(\hat{y}^{(i)}, y^{(i)})=-\frac{1}{m}\sum\limits_{i=1}^{m}\Big[y^{(i)} \; \mathrm{log} \; \hat{y}^{(i)}+(1-y^{(i)}) \; \mathrm{log} \; (1-\hat{y}^{(i)})\Big]\)
Want to find \(w, b\) that minimize \(J(w, b)\)
\(w:=w-\alpha\frac{\partial J(w,b)}{\partial w}\)
\(b:=b-\alpha\frac{\partial J(w,b)}{\partial b}\)
同时更新(simultaneously update)\(w, b\)
导数(Derivatives)
一个函数 \(f(a)=3a\), 它是一条直线。
假定 \(a=2\),那么 \(f(a)\) 是 \(a\) 的3倍等于6,也就是说如果 \(a=2\),那么函数 \(f(a)=6\)。
假定稍微改变一点点 \(a\) 的值,只增加一点,变为 2.001,这时 \(a\) 将向右做微小的移动。现在 \(f(a)\) 等于 \(a\) 的3倍是 6.003。
请看绿色高亮部分的这个小三角形,如果向右移动 0.001,那么 \(f(a)\) 增加 0.003,\(f(a)\) 的增量是 \(a\) 的增量的 3 倍。
因此我们说函数 \(f(a)\) 在 \(a=2\) 的导数是这个点的斜率,或者说,当 \(a=2\) 时,斜率是3。
更正式的斜率定义为在上图这个绿色的小三角形中,高除以宽。即斜率等于 0.003 除以 0.001,等于3。
或者说导数等于3,这表示当你将 \(a\) 右移0.001,\(f(a)\) 的值增加3倍水平方向的量。
导数的数学定义是,如果你右移很小的 \(a\) 值(不是0.001,而是一个无限小的值),\(f(a)\) 会增加这个无限小的值的 3 倍。
这个函数(一条直线)在所有地方的斜率都相等,本例中都等于 3。在下一节我们看一个更复杂的例子,这个例子中函数在不同点的斜率是可变的。
更多的导数例子(More Derivative Examples)
曲线上,不同位置对应的斜率会不同,不同位置画一些小小的三角形你就会发现,三角形高和宽的比值,在曲线上不同的点,比值也不同。
具体可以参考高等数学中的导数公式来计算。
最后记住两点:
第一点,导数就是斜率,而函数的斜率,在不同的点是不同的。
第二点,如果你想知道一个函数的导数,你可参考你的微积分课本或者维基百科,然后你应该就能找到这些函数的导数公式。
计算图(Computation Graph)
可以说,一个神经网络的计算,都是按照前向或反向传播过程组织的。
首先我们计算出一个新的网络的输出(前向过程),紧接着进行一个反向传输操作。
后者我们用来计算出对应的梯度或导数。
计算图解释了为什么我们用这种方式组织这些计算过程。
下面,我们将举一个例子说明计算图是什么。
让我们举一个比逻辑回归更加简单的,或者说不那么正式的神经网络的例子。
我们尝试计算函数 \(J\),\(J\) 是由三个变量 \(a,b,c\) 组成的函数,这个函数是 \(3(a+bc)\)。计算这个函数实际上有三个不同的步骤,首先是计算 \(b\) 乘以 \(c\),我们把它储存在变量 \(u\) 中,因此 \(u=bc\); 然后计算 \(v=a+u\);最后输出 \(J=3v\),这就是要计算的函数 \(J\)。
我们可以把这三步画成如下面的计算图:
从这个小例子中我们可以看出,通过一个从左向右的过程,你可以计算出 \(J\) 的值。为了计算导数,从右到左(红色箭头,和蓝色箭头的过程相反)的过程是用于计算导数最自然的方式。 概括一下:计算图组织计算的形式是用蓝色箭头从左到右的计算,让我们看看下一节中如何进行反向红色箭头(也就是从右到左)的导数计算。
计算图导数(Derivatives with a Computation Graph)
\(\frac{dJ}{du}=\frac{dJ}{dv} \cdot \frac{dv}{du}\)
\(\frac{dJ}{da}=\frac{dJ}{dv} \cdot \frac{dv}{da}\)
\(\frac{dJ}{db}=\frac{dJ}{du} \cdot \frac{du}{db}=\frac{dJ}{dv} \cdot \frac{dv}{du} \cdot \frac{du}{db}\)
\(\frac{dJ}{dc}=\frac{dJ}{du} \cdot \frac{du}{dc}=\frac{dJ}{dv} \cdot \frac{dv}{du} \cdot \frac{du}{dc}\)
神经网络反向传播算法使用计算图来求导数,其实就是多元复合函数的链式求导法则。
逻辑回归的梯度下降(Logistic Regression Gradient Descent)
下面使用计算图对梯度下降算法进行计算。我必须要承认的是,使用计算图来计算逻辑回归的梯度下降算法有点大材小用了。但是,我认为以这个例子作为开始来讲解,可以使你更好的理解背后的思想。从而在讨论神经网络时,你可以更深刻而全面地理解神经网络。接下来让我们开始学习逻辑回归的梯度下降算法。
\(z=w^{\top}x + b\)
\(\hat{y}=a=\sigma(z)\)
\(\mathcal{L}(a,y)=-(y \; \mathrm{log} \; (a) + (1-y) \; \mathrm{log} \; (1-a))\)
\(da=\frac{d\mathcal{L}}{da}=-\frac{y}{a}+\frac{1-y}{1-a}\)
\(\frac{da}{dz}=a(1-a)\), 这个是 sigmoid 函数对 \(z\) 求导
\(dz=\frac{d\mathcal{L}}{dz}=\frac{d\mathcal{L}}{da} \cdot \frac{da}{dz}=\Big(-\frac{y}{a}+\frac{1-y}{1-a}\Big) \cdot \Big(a(1-a)\Big)=a-y\)
\(dw_1=\frac{\partial \mathcal{L}}{\partial w_1}=x1 \cdot dz\)
\(dw_2=\frac{\partial \mathcal{L}}{\partial w_2}=x2 \cdot dz\)
\(db=\frac{\partial \mathcal{L}}{\partial w_1}=dz\)
梯度下降更新:
\(w_1:=w_1-\alpha dw_1\)
\(w_2:=w_1-\alpha dw_2\)
\(b:=b-\alpha db\)
训练逻辑回归模型不仅仅只有一个训练样本,而是有个 \(m\) 训练样本的整个训练集,下一节探讨讲这些思想应用到整个训练集中。
梯度下降的例子(Gradient Descent on m Examples)
\(J(w, b) = \frac{1}{m}\sum\limits_{i=1}^{m}\mathcal{L}(\hat{y}^{(i)}, y^{(i)})\)
\(a^{(i)}=\hat{y}^{(i)}=\sigma(z^{(i)})=\sigma(w^{\top}x^{(i)}+b)\)
\(dw_1=\frac{\partial}{\partial w_1}J(w,b)=\frac{1}{m} \sum\limits_{i=1}^{m} \; dw_1^{(i)}=\frac{1}{m} \sum\limits_{i=1}^{m} \; \frac{\partial}{\partial w_1}\; \mathcal{L}(a^{(i)},y^{(i)})\)
\(dw_2=\frac{\partial}{\partial w_2}J(w,b)=\frac{1}{m} \sum\limits_{i=1}^{m} \; dw_2^{(i)}=\frac{1}{m} \sum\limits_{i=1}^{m} \; \frac{\partial}{\partial w_2}\; \mathcal{L}(a^{(i)},y^{(i)})\)
\(db=\frac{\partial}{\partial b}J(w,b)=\frac{1}{m} \sum\limits_{i=1}^{m} \; db^{(i)}=\frac{1}{m} \sum\limits_{i=1}^{m} \; \frac{\partial}{\partial b}\; \mathcal{L}(a^{(i)},y^{(i)})\)
从逻辑回归的代价函数和梯度公式可以看出,它们都是需要累加的,下面的代码用 for 循环实现累加,并最终计算一次 w,b 的更新。
\(J=0;\;\;dw_1=0;\;\;dw_2=0;\;\;db=0;\)
For \(i\) = 1 to \(m\):
\(\quad\quad z^{(i)}=w^{\top}x^{(i)}+b\)
\(\quad\quad a^{(i)}=\sigma(z^{(i)})\)
\(\quad\quad J+=-\Big[y^{(i)} \; \mathrm{log} \; a^{(i)} + (1-y^{(i)}) \; \mathrm{log} \; (1-a^{(i)})\Big]\) // totalizer
\(\quad\quad dz^{(i)}=a^{(i)}-y^{(i)}\)
\(\quad\quad dw1+=x_1^{(i)}\;dz^{(i)}\) // totalizer
\(\quad\quad dw2+=x_2^{(i)}\;dz^{(i)}\) // totalizer
\(\quad\quad db+=dz^{(i)}\) // totalizer
\(J/=m;\;\;dw_1/=m;\;\;dw_2/=m;\;\;db/=m;\)
// simultaneously update
\(w_1:=w_1-\alpha \; dw_1\)
\(w_2:=w_2-\alpha \; dw_2\)
\(b:=b-\alpha \; db\)
以上(say \(n\) = 2)只应用了一步梯度下降。因此你需要重复以上内容很多次,以应用多次梯度下降。
代码中显式地使用for循环使你的算法很低效。在深度学习兴起之前,向量化是很棒的,可以使你有时候加速你的运算,但有时候也未必能够。但是在深度学习时代使用向量化来摆脱for循环已经变得相当重要。因为我们越来越多地训练非常大的数据集,因此你真的需要你的代码变得非常高效。接下来的几节课中,我们会谈到向量化,以及如何应用向量化而连一个for循环都不使用。
向量化(Vectorization)
For compute \(z=w^{\top}x+b\)
for loop version:
z=0
for i in range(n_x):
z+=w[i]*x[i]
z+=b
vectorization version:
z=np.dot(w.T, x) + b
效率对比:
import numpy as np #导入numpy库
a = np.array([1,2,3,4]) #创建一个数据a
print(a)
# [1 2 3 4]
import time #导入时间库
a = np.random.rand(1000000)
b = np.random.rand(1000000) #通过round随机得到两个一百万维度的数组
tic = time.time() #现在测量一下当前时间
#向量化的版本
c = np.dot(a,b)
toc = time.time()
print(“Vectorized version:” + str(1000*(toc-tic)) +”ms”) #打印一下向量化的版本的时间
#继续增加非向量化的版本
c = 0
tic = time.time()
for i in range(1000000):
c += a[i] * b[i]
toc = time.time()
print(c)
print(“For loop:” + str(1000*(toc-tic)) + “ms”)#打印for循环的版本的时间
结果:
250286.989866
Vectorized version : 1.5027523040771484 ms
250286.989866
For loop : 474.29513931274414 ms
你可能听过很多类似如下的话,“大规模的深度学习使用了 GPU 或者图像处理单元实现”,但是我做的所有的案例都是在 jupyter notebook 上面实现,这里只有 CPU,CPU 和 GPU 都有并行化的指令,他们有时候会叫做 SIMD 指令,这个代表了一个单独指令多维数据,这个的基础意义是,如果你使用了 built-in 函数,像 np.function
或者并不要求你实现循环的函数,它可以让 python 的充分利用并行化计算,实际是在 GPU 和 CPU 上面计算,GPU 更加擅长 SIMD 计算,但 CPU 其实也不是太差,可能没有 GPU 那么擅长吧。
接下来的课程,你将看到向量化是如何加速你的代码的,经验法则是,无论什么时候,避免使用明确的 for 循环。
更多的向量化例子(More Examples of Vectorization)
numpy 中有许多实现了并行化的 function 可以直接调用
下面,优化一下之前的代码,把 \(dw\) 先向量化,也算是省去了一个 1:n 的循环,直接用 \(dw += x^{(i)}\;dz^{(i)}\) 代替了。
向量化逻辑回归(Vectorizing Logistic Regression)
Z = np.dot(w.T, X) + b
b.shape = (1,1), 相加时会自动变成 (1,m) 维,这是python 的广播(broadcasting)机制。
向量化逻辑回归的梯度计算(Vectorizing Logistic Regression's Gradient)
for iter in range(epoch):
Z = np.dot(w.T, X) + b # shape(1, m)
A = sigma(Z) # shape(1, m)
dZ = A - Y # shape(1, m)
dw = X * dZ.T / m # shape(n, 1)
db = np.sum(dZ) / m # shape(1, 1)
w = w - alpha * dw # shape(n, 1)
b = b - alpha * db # shape(1, 1)
最外层迭代次数的 for 循环无法省略,每次梯度更新没有任何 for 循环已经很爽了。
Python中的广播机制(Broadcasting in Python)
In [1]: import numpy as np
In [2]: A = np.array([[56.0,0.0,4.4,68.0],
...: [1.2,104.0,52.0,8.0],
...: [1.8,135.0,99.0,0.9]])
In [3]: print(A)
[[ 56. 0. 4.4 68. ]
[ 1.2 104. 52. 8. ]
[ 1.8 135. 99. 0.9]]
In [4]: cal = A.sum(axis=0)
In [5]: print(cal)
[ 59. 239. 155.4 76.9]
In [6]: percentage = 100 * A / cal.reshape(1, 4)
In [7]: print(percentage)
[[94.91525424 0. 2.83140283 88.42652796]
[ 2.03389831 43.51464435 33.46203346 10.40312094]
[ 3.05084746 56.48535565 63.70656371 1.17035111]]
当我们写代码时不确定矩阵维度的时候,通常会对矩阵进行 reshape 来确保得到我们想要的列向量或行向量。
reshape 是一个常量时间的操作,时间复杂度是 \(\mathcal{O}(1)\),它的调用代价极低。
axis=0 是对列求和,如果 axis=1,则是对行求和。
General Principle:
(m, n) matrix + - * / (1, n) = (m, n)
(m, n) matrix + - * / (m, 1) = (m, n)
(m, 1) + \(\mathbb{R}\)
\(\begin{bmatrix}1\\2\\3\end{bmatrix}\) + 100 = \(\begin{bmatrix}101\\102\\103\end{bmatrix}\)
\(\begin{bmatrix}1&2&3\end{bmatrix}\) + 100 = \(\begin{bmatrix}101&102&103\end{bmatrix}\)
关于 Python与numpy向量的使用(A note on python or numpy vectors)
Python的特性允许你使用广播(broadcasting)功能,这是Python的numpy程序语言库中最灵活的地方。而我认为这是程序语言的优点,也是缺点。优点的原因在于它们创造出语言的表达性,Python语言巨大的灵活性使得你仅仅通过一行代码就能做很多事情。但是这也是缺点,由于广播巨大的灵活性,有时候你对于广播的特点以及广播的工作原理这些细节不熟悉的话,你可能会产生很细微或者看起来很奇怪的bug。例如,如果你将一个列向量添加到一个行向量中,你会以为它报出维度不匹配或类型错误之类的错误,但是实际上你会得到一个行向量和列向量的求和。
In [1]: import numpy as np
In [2]: a = np.random.randn(5)
In [3]: a
Out[3]: array([-1.75815016, -1.07273969, 0.09507771, -0.92821468, -0.51926189])
In [4]: a.shape
Out[4]: (5,)
# (5,) 这是一个秩为1的数组(a rank 1 array)
In [5]: a.T # 转置了还是和原来一样
Out[5]: array([-1.75815016, -1.07273969, 0.09507771, -0.92821468, -0.51926189])
In [6]: np.dot(a, a.T)
Out[6]: 5.382117583277082 # 你以为结果会是一个矩阵,但却是一个实数
这种秩为 1 的数组经常会导致 bug 的出现,所以重点的重点是在编写程序时摒弃掉这种数组。
可以有几种方法参考:
- 使用 assert(a.shape == (5, 1)),速度快,可以在程序中随意插入。
- 使用 reshape,显性的转换成自己预期的形状
- 使用 np.random.randn(5, 1) or np.random.randn(1, 5),直接初始化成向量,不要使用 np.random.randn(5) 这种会产生秩为 1 的数组的方式
In [1]: import numpy as np
In [2]: a = np.random.randn(5)
In [3]: a
Out[3]: array([-1.75815016, -1.07273969, 0.09507771, -0.92821468, -0.51926189])
In [4]: a.shape
Out[4]: (5,)
In [8]: a.reshape(5,1)
Out[8]:
array([[-1.75815016],
[-1.07273969],
[ 0.09507771],
[-0.92821468],
[-0.51926189]])
In [9]: a.reshape(1,5)
Out[9]: array([[-1.75815016, -1.07273969, 0.09507771, -0.92821468, -0.51926189]])
In [10]: assert(a.shape == (1, 5))
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-10-037c48ad2a2c> in <module>
----> 1 assert(a.shape == (1, 5))
AssertionError:
In [11]: assert(a.shape == (5, 1))
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-11-ffa475e738c6> in <module>
----> 1 assert(a.shape == (5, 1))
AssertionError:
In [12]: a = a.reshape(1, 5)
In [13]: assert(a.shape == (1, 5))
In [14]: np.random.randn(1, 5)
Out[14]: array([[ 0.30362665, -1.20299342, -0.03064048, -0.33053973, 0.27108289]])
In [15]: np.random.randn(5)
Out[15]: array([ 0.39712751, -0.23749248, 0.76824738, 0.73690755, 0.6967762 ])
In [16]: np.random.randn(1, 5).shape
Out[16]: (1, 5)
In [17]: np.random.randn(5).shape
Out[17]: (5,)
Jupyter/iPython Notebooks快速入门(Quick tour of Jupyter/iPython Notebooks)
略
逻辑回归损失函数详解(Explanation of logistic regression cost function)
下面,根据最大似然估计推导逻辑回归的损失函数(直观理解 : 即求出一组参数,使式子取最大值)。
if \(y = 1: P(y \; | \; x) = \hat{y}\)
if \(y = 0: P(y \; | \; x) = 1 - \hat{y}\)
合并:
\(P(y \; | \; x) = \hat{y}^y \;\; (1-\hat{y})^{(1-y)}\)
添加 log 函数,将 \(P\) 的输出限制在 0 和 1 之间
\(\mathrm{log} \; P(y \; | \; x) = \mathrm{log} \; \hat{y}^y \; (1-\hat{y})^{(1-y)} = y \; \mathrm{log} \; \hat{y} + (1 - y) \; \mathrm{log} \; (1 - \hat{y})\)
$\mathrm{log} ; P(y ; | ; x) $ 就是我们的 loss function,单个训练样本的情况。
即 :$\mathcal{L}(\hat{y}, y) = \mathrm{log} ; P(y ; | ; x) $
但此时 \(\mathrm{log} \; P(y \; | \; x)\) 的图像并不是像上图中所示的样子,而是图中沿 x 轴翻转的。
此时的含义是:
- if y = 1,只有令 \(\mathcal{L}(\hat{y}, y)\) 最大,输出的条件概率才会趋近于 1
- if y = 0,只有令 \(\mathcal{L}(\hat{y}, y)\) 最大,输出的条件概率才会趋近于 0
而我们使用梯度下降算法,需要最小化 \(\mathcal{L}(\hat{y}, y)\) 来求 y = 1 的条件概率,理论上概率应该趋近于 1,但此时 loss function 是趋近于 0 的。
所以 ,我们需要在 \(\mathcal{L}(\hat{y}, y)\) 前面添加一个负号,令其沿 x 轴翻转,即变成上图所示的样子。
那么它的含义就变成我们想要的样子了:
- if y = 1,令 \(\mathcal{L}(\hat{y}, y)\) 最小,输出的条件概率趋近于 1
- if y = 0,令 \(\mathcal{L}(\hat{y}, y)\) 最小,输出的条件概率趋近于 0
所以改写 \(\mathcal{L}(\hat{y}, y)\),添加负号:
\(\mathcal{L}(\hat{y}, y) = - \Big(y \; \mathrm{log} \; \hat{y} + (1 - y) \; \mathrm{log} \; (1 - \hat{y})\Big)\)
以上是单个样本时的情况,下面看看 m 个样本时的情况。
假设所有的训练样本服从同一分布且相互独立,也即独立同分布的,所有这些样本的联合概率就是每个样本概率的乘积,即:
\(\mathrm{log} \; P(\mathrm{labels\;\;in\;\;training\;\;set}) = \mathrm{log}\; \prod\limits_{i=1}^{m} P(y^{(i)},x^{(i)})=\sum\limits_{i=1}^{m} \; \mathrm{log}\; P(y^{(i)},x^{(i)})\)
添加负号之后变成:
\(\sum\limits_{i=1}^{m} \; \mathcal{L}(\hat{y}, y) = \sum\limits_{i=1}^{m} \; - \Big(y \; \mathrm{log} \; \hat{y} + (1 - y) \; \mathrm{log} \; (1 - \hat{y})\Big)\)
把负号移到前面,再取平均值后:
\(J(w, b)=\sum\limits_{i=1}^{m} \; \mathcal{L}(\hat{y}, y) = - \frac{1}{m} \; \sum\limits_{i=1}^{m} \; y \; \mathrm{log} \; \hat{y} + (1 - y) \; \mathrm{log} \; (1 - \hat{y})\)