Logistic回归

Logistic回归

一些约定和基础

一般约定,x的上标(i)表示第i个样本;在矩阵中表示样本,通常将样本各个维度的特征写成列向量,一列就是一个样本的各个特征。
那么Y矩阵就是一个1*m矩阵,m是样本数目。还约定n_x为X中样本特征的维度。在python里的表示为

1
Y.shape  # (1, m)

在Logistic回归中,我们总希望通过z = w.transpose * x + b获得每个x(i)的预测值y-hat(i),而且我们希望y-hat(i)尽量接近y(i).这里的y-hat和概率论中的参数估计类似。
如果想要把y-hat概率化,我们需要sigmoid函数作用到上式结果中。即y-hat = sigmoid(z).

通常我们需要standardize图像。在图像的standardization通常是对flatten之后的向量/255。


损失函数

这里的损失函数是y和y-hat的二元函数。我们通常不使用均方差,这会出问题。在这里,我们的损失函数定义成

L(y, y-hat) = - (y * ln(y-hat) + (1 - y) *  ln(1 - y-hat))

lost总是对单一一个样本说的。如果要对所有样本看来,定义cost函数为
J = Σ(L(y, y-hat)) / m # 其中m是样本总数
我们要做的就是最小化J。


梯度下降法

这被用于更新w和b参数。类似牛顿迭代法,我们有

w := w - α * dw  # dw是Loss函数在w这一点的导数或者偏导数
b := b - α * db

在梯度下降法中,每个导数都是J或者L对于参数的导数
具体到Logistic回归里面,我们的过程简化为两个样本的回归。过程为

(x1, w1, x2, w2, b) -> 
z = w1x1 + w2x2 + b -> 
a = sigmoid(z) ->  # 这里写成a容易表示
L(a, y)

刚刚说到,每个导数都是J或者L对于参数的导数。我们在反向传播要用到L对各个参数的导数,导数的表示法同上。具体为

1
2
3
4
5
da = -y/a + (1-y) / (1-a)  # 这可以根据L的函数表达式直接求导得来
dz = dL/da * da/dz = a - y,其中dL/da就是上一条的结果,da/dz可以根据sigmoid的函数表达式求导得来
dw1 = x1 * dz
dw2 = x2 * dz
db = dz

很明显,上述式子都是基于两个样本的,导数都是L的。如果要做成J的导数,就应该将所有值初始化为0,遍历m次,每次都做累加最后除以m就是J的导数。
说白了,分析过程是从L的导数然后再累加取平均,以这样的方式求出J的导数。
任何的深度学习导数计算都应该像这样计算。
最后得出来的结果伪代码是:

1
2
3
4
5
6
7
8
9
10
J = 0; dw1 = 0; dw2 = 0; db = 0;
for i in range(1, m + 1):
z[i] = w.transpose * x[i] + b
a[i] = sigmoid(z[i])
J += L(y[i], a[i])
dz[i] += a[i] - y[i]
dw1 += x1[i] * dz[i] # 第i个样本的第一个特征
dw2 += x2[i] * dz[i]
db += dz[i]
J /= m; dw[1] /= m; dw2 /= m; db /= m

向量化

将各个导数算完之后,考虑对计算的优化,即向量化。这可以减少显示调用for的次数,提高性能。
在np中,将dw1和dw2初始化为0的列向量,只需要dw = np.zeros((n_x, 1)),其中param1是列。
现在看到X矩阵,它是一个(n_x, m)的矩阵。而且w是个m维行向量。我们不再像上面使用x1 x2这样的记号,将计算z的过程简化为

z = w.transpose.dot(x) + b  # 这里b是个数,但np会自动转化成m维行向量;算出来的z也是m维行向量

以此类推,我们可以将上一个结果全都向量化,代码为

1
2
3
4
5
6
7
8
9
10
w = np.zeros((n_x, 1))
b = 0 # np.zeros((1, m))
for iter in range(1000): # 梯度下降1000次
Z = w.transpose.dot(X) + b
A = sigmoid(Z)
dZ = A - Y # Y也是(1, m)的向量
dw = X.dot(dZ.transpose) / m
db = np.sum(dZ) / m # np的built-in方法也可以对np数组使用
w -= lr * dw
b -= lr * db

一些关于np的trick

比如说,有一个矩阵A = [[1, 2, 3, 4],
[2, 2, 3, 4],
[3, 2, 3, 4]]
我们想要按列加,得到[6, 6, 9, 12],只需写入

A = A.sum(axis=0)  # 0代表竖着

如果要算对应位置在和数里占的百分比,写成

temp = A.sum(axis=0)
per = temp / temp.reshape(1, 4)  # 首先,这个reshape可以确保矩阵的形状,没事可以多用用;这里还有broadcast内容,即3\*4矩阵除以1\*4矩阵,结果还是我们想要的3\*4。

np随机数数组的写法是np.random.randn(99),这生成一个99个数的数组。但这是不安全的。更安全的做法是写成np.random.randn(99, 1),这生成一个真正的矩阵,而不是数组。数组连向量都算不上。
还有在当不确定矩阵维数的时候,随意使用assert就好了,这对性能没有影响。即

assert(a.shape == (m, n))

np提供了归一化,即normalization的方法。例如x = [[0, 3, 4], [0, 5, 12]],通过

1
norm = np.linalg.norm(x, axis=1, keepdims=True)  # axis=1即为横向,算出来就是[[5], [13]]

np.squeeze可以将一维的数组内容取出来,比如[[1]]经过squeeze就是[1],[1]经过squeeze就是1,注意到取出来之后仍然类型是array,但已经可以用来当索引。

通常,对于(m, px_w, px_h, 3)的图像,我们在处理的时候需要把它变成数组。那么就应该调用:

1
X_flatten = X.reshape(X.shape[0], -1).T  # 这里-1是让np自己去算剩下那个维度的大小,转置之后就是想要的每一列都是一个样本

逆天,np的zeros必须括号里套括号传tuple,而random.randn只能一层括号。