Multiple Linear Regression

Multiple Linear Regression

在前一部分,我们研究了单个特征的线性回归,进一步,我们想要研究具有多个特征的线性回归模型。

Terminology

x j x_j xj:第j个特征

n:特征的数量

x ⃗ ( i ) \vec x^{(i)} x (i):第i个训练数据的特征集

x ⃗ j ( i ) \vec x_j^{(i)} x j(i):第i个数据的第j个特征

Model

对于多个特征的模型,其线性回归模型的表达式为
f w , b = w 1 x 1 + w 2 x 2 + . . . + w n x n + b = ∑ j = 1 n w j x j + b f_{w, b} = w_1x_1 + w_2x_2 + ... + w_nx_n + b = \sum_{j = 1}^n w_jx_j + b fw,b=w1x1+w2x2+...+wnxn+b=j=1nwjxj+b
用向量的方式表示
w ⃗ = [ w 1 , w 2 , . . . , w n ] x ⃗ = [ x 1 , x 2 , . . . , x n ] f w ⃗ , b = w ⃗ ⋅ x ⃗ + b = w ⃗ x ⃗ T + b \vec w = [w_1, w_2, ..., w_n] \newline \vec x = [x_1, x_2, ..., x_n] \newline f_{\vec w, b} = \vec w \cdot \vec x + b = \vec w \vec x^T + b w =[w1,w2,...,wn]x =[x1,x2,...,xn]fw ,b=w x +b=w x T+b
这种技巧称为矢量化(vectorization)。

离散特征的量化

类似“房屋面积:房屋价格”这种连续的特征关系,量化较为容易;而实际场景中,有很多特征都是离散的,在此讨论下如何量化离散特征

  • 对只有“是”或“否”的离散特征,可以使用0,1来表示,称为二值离散特征
  • 对于有多个离散值,且离散值之间存在顺序关系的,例如饭量“大”,“中”,“小”,可以使用有序的离散值来表示,称为有序的多值离散特征(“大”:3,“中”:2,“小”:1)
  • 对有多个离散值,离散值之间不具有顺序关系,例如肤色“黄”,”白“,”黑“,不能直接分配有序的离散值,而是应该拆成多个二值离散特征进行表示(黄:[1,0,0],白:[0,1,0],黑:[0,0,1])

Vectorization

Code Implement

设有如下参数

在这里插入图片描述

在Numpy中,可以这样表示

import numpy as np
w = np.array([1.0, 2.5, -3.3])
b = 4
x = np.array([10, 20, 30])

通过向量化的, f w ⃗ , b f_{\vec w, b} fw ,b可以实现为

f_wb = np.dot(w, x) + b

使用向量化运算有两个好处:

  • 代码更加整洁可读
  • Numpy底层可以并行处理,效率更快

How could vectorization run much more faster than simply loop

当采用loop实现方法的时候,程序会顺序执行每个f += w[i] * x[i]

在这里插入图片描述

而在向量化实现中,计算机可以同时并行计算多个w[i] * x[i]

在这里插入图片描述

以上是使用线性代数库计算性能的直观表示

Gradient Descent

首先关注多个feature,假定b=0,此时对多个 w j w_j wj,存在同样数量的导数 d j d_j dj,同样考虑一个向量化的实现方式。

w = np.array([...])
d = np.array([...])
alpha = 0.1 # 学习率
# loop实现
for j in range(n):
    w[j] = w[j] - alpha * d[j]
# 向量化实现
w = w - alpha * d

NumPy和Python补充

创建array

转换Python序列**(List, Tuple)**为NumPy Array

import numpy as np
a1 = np.array([1, 2, 3, 4]) # 创建1D array
a2 = np.array([1, 2], [3, 4]) # 创建2D array

List或Tuple的嵌套会创建高维的数组。所有的array对象在NumPy中都称为ndarray。即n-dimensional array

你可以显式指定数组元素的数据类型,使用dtype参数;指定参数类型要注意别越界,例如

a = np.array([127128129], dtype = np.int8)
'''
array([ 127, -128, -127], dtype=int8)
'''

当使用两个不同dtype的array进行计算时,结果矩阵的数据类型会匹配为能够兼容两种dtype的类型。

内置的矩阵创建函数

NumPy包含40多个内置的函数可以创建矩阵(Array creation routines)。这些函数可以根据数组的维数,大致分为3类:

  • 1D arrays
  • 2D arrays
  • ndarrays

这里先列出吴恩达老师提到的课程要用的那些

1D array
# np.arange((start = 0, )end(, step = 1)) # 返回从start开始,end结束,步长为step的分布数组, start和step可缺省
>>> np.arange(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> np.arange(2, 10, dtype = float)
array([2., 3., 4., 5., 6., 7., 8., 9.])
>>> np.arange(2, 3, 0.1)
array([2. , 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9])

# np.linspace(start, end, num) # 返回从start开始,end结束,长度为num的数组,step呈均匀分布
>>> np.linspace(1., 4., 6)
array([1. , 1.6, 2.2, 2.8, 3.4, 4. ])
nD array
# np.zeros(shape: Tuple) # 生成多维array,参数shape是一个元组,指定array的形状,填充0
>>> np.zeros((2,))
array([0., 0.])
>>> np.zeros((2, 3, 2))
array([[[0., 0.],
        [0., 0.],
        [0., 0.]],

       [[0., 0.],
        [0., 0.],
        [0., 0.]]])
# np.ones(shape: Tuple) # 生成多维array, shape是元组,指定array的形状,填充1
>>> np.ones((2, 3, 2))
array([[[1., 1.],
        [1., 1.],
        [1., 1.]],

       [[1., 1.],
        [1., 1.],
        [1., 1.]]])
# np.random.random_sample(shape: Tuple) # 生成多维array,填充[0, 1]的浮点
>>> np.random.random_sample((2, 3, 2))
array([[[0.58886443, 0.00125135],
        [0.55973185, 0.25874808],
        [0.03675558, 0.7065201 ]],

       [[0.72083918, 0.13066142],
        [0.50732886, 0.37403505],
        [0.16216774, 0.07918865]]])
copy

当需要将一个array中的数据赋给一个新的变量的时候,需要显式调用numpy.copy,执行深拷贝,否则新变量指向的是原始array的数据。

给出一个错误的例子

>>> a = np.array([1, 2, 3, 4, 5, 6])
>>> b = a[:2]
>>> b += 1
>>> print('a =', a, '; b =', b)
a = [2 3 3 4 5 6] ; b = [2 3]

在这个例子中,没有执行深拷贝,因此b指向a的原始数据,当执行b+=1的时候,会修改a中的值。或者说,这里创建的b是a的一个视图(view),视图相当于对原始数据的一个子集的索引。

正确的写法

>>> a = np.array([1, 2, 3, 4])
>>> b = a[:2].copy()
>>> b += 1
>>> print('a = ', a, 'b = ', b)
a =  [1 2 3 4] b =  [2 3]

operation

Index 索引

使用[]索引array中的元素,索引从0到n-1,索引不可以越界

>>> a = np.arange(10)
>>> print(a[2])
2

Slicing 切片

使用(start = 0: stop = n-1: step = 1)获取数组的一个子集,注意[start, stop)左包含右不包含

>>> a = np.arange(10)
>>> c = a[2: 7: 2]; print(c)
[2 4 6]
>>> c = a[3:]; print(c)
[3 4 5 6 7 8 9]
>>> c = a[:-1]; print(c)
[0 1 2 3 4 5 6 7 8]
>>> c = a[:]; print(c)
[0 1 2 3 4 5 6 7 8 9]

同样可以对n维数组进行切片,多个(start = 0: stop = n-1: step = 1)之间用,隔开。

>>> a = np.arange(20).reshape(-1, 10)
>>> print(a)
[[ 0  1  2  3  4  5  6  7  8  9]
 [10 11 12 13 14 15 16 17 18 19]]
>>> #access 5 consecutive elements (start:stop:step)
... print("a[0, 2:7:1] = ", a[0, 2:7:1], ",  a[0, 2:7:1].shape =", a[0, 2:7:1].shape, "a 1-D array")
a[0, 2:7:1] =  [2 3 4 5 6] ,  a[0, 2:7:1].shape = (5,) a 1-D array
>>> #access 5 consecutive elements (start:stop:step) in two rows
... print("a[:, 2:7:1] = \n", a[:, 2:7:1], ",  a[:, 2:7:1].shape =", a[:, 2:7:1].shape, "a 2-D array")
a[:, 2:7:1] = 
 [[ 2  3  4  5  6]
 [12 13 14 15 16]] ,  a[:, 2:7:1].shape = (2, 5) a 2-D array
>>> # access all elements
... print("a[:,:] = \n", a[:,:], ",  a[:,:].shape =", a[:,:].shape)
a[:,:] = 
 [[ 0  1  2  3  4  5  6  7  8  9]
 [10 11 12 13 14 15 16 17 18 19]] ,  a[:,:].shape = (2, 10)
>>> # access all elements in one row (very common usage)
... print("a[1,:] = ", a[1,:], ",  a[1,:].shape =", a[1,:].shape, "a 1-D array")
a[1,:] =  [10 11 12 13 14 15 16 17 18 19] ,  a[1,:].shape = (10,) a 1-D array
>>> # same as
... print("a[1]   = ", a[1],   ",  a[1].shape   =", a[1].shape, "a 1-D array")
a[1]   =  [10 11 12 13 14 15 16 17 18 19] ,  a[1].shape   = (10,) a 1-D array

Single vector

NumPy支持对向量执行sum(),mean()等操作,包括对向量中的元素执行逐元素的操作(例如倍乘,或者说数乘向量)

>>> a = np.arange(4)
>>> print(-a)
[ 0 -1 -2 -3]
>>> print(np.sum(a))
6
>>> print(np.mean(a))
1.5
>>> print(a**2)
[0 1 4 9]
>>> print(a * 5)
[ 0  5 10 15]

向量和差

NumPy支持两个等shape向量求和差,如果不同shape的话,会尝试进行向量广播(Boardcast),广播可能导致不可预知的后果。

>>> a = np.arange(4)
>>> b = np.arange(5, 9)
>>> print(a, b, a + b)
[0 1 2 3] [5 6 7 8] [ 5  7  9 11]
>>> a = np.arange(4)
>>> b = np.ones((2, 4))
>>> print(a); print(b)
[0 1 2 3]
[[1. 1. 1. 1.]
 [1. 1. 1. 1.]]
>>> print(a + b)
[[1. 2. 3. 4.]
 [1. 2. 3. 4.]]

dot

NumPy的.dot()在底层会通过并行计算加速运算,所以不要自己写个loop代替。

>>> a = np.array([1, 2, 3, 4])
>>> b = np.array([-1, 4, 3, 2])
>>> c = np.dot(a, b); print(c)
24
>>> c = np.dot(b, a); print(c)
24

dot的结果是一个标量(数),同时,正如你在线性代数课程上所学到的,dot满足交换律,具体可以参考3b1b《线性代数的本质》

reshape

将ndarray的shape修改,不修改内部元素。

numpy.reshape(a, newshape, order={'C', 'F', 'A'})
'''
	parameters: 
		a: array_like # Array to be reshaped
		newshape: int or tuple of ints # new shape should be compatible with the original shape. 
									# one shape dimension can be -1, so the value will be inferred
		order: {'C','F','A'} # different index order
						   # 'C' means using C-like order
						   # 'F' means using Fortran-like order
						   # 'A' means auto choice
	returns
		reshaped_array: ndarray
'''
>>> a = np.arange(6).reshape((-1,2))
>>> print(f'{a.shape}, \n{a}')
(3, 2), 
[[0 1]
 [2 3]
 [4 5]]

数据集初步观察

对拿到的数据集,可以先绘制每个特征与目标值之间的关系图,例如,对一个房屋价格预测模型,有4个特征(size, bedrooms, floors, age),通过matplotlib绘图

import matplotlib.pyplot as plt
# load dataset
X_train, y_train = load_house_data()
X_features = ['size(sqft)','bedrooms','floors','age']
# plotting each feature versus price
fig,ax=plt.subplots(1, 4, figsize=(12, 3), sharey=True)
for i in range(len(ax)):
    ax[i].scatter(X_train[:,i],y_train)
    ax[i].set_xlabel(X_features[i])
ax[0].set_ylabel("Price (1000's)")
plt.show()

在这里插入图片描述

如上,可以粗略观察到,房屋的面积和价格呈正相关。新房屋通常比旧房屋价格高。而卧室数量和层数没有看出明显影响。

Gradient Descent for Multiple Regression

类似单元线性回归,多元线性回归需要迭代更新 w i w_i wi b b b,从而最小化损失函数

在这里插入图片描述

Multiple Variables Linear Prediction

多元线性模型的向量表示如下
f w , b = w ⃗ ⋅ x ⃗ + b f_{w, b} = \vec w \cdot \vec x + b fw,b=w x +b
在代码中的实现

import numpy as np
f_wb = np.dot(w, x) + b

Cost function

多元线性模型的损失函数可以表示为
J ( w ⃗ , b ) = 1 2 m ∑ i = 0 m − 1 ( f w , b ( x ⃗ i ) − y i ) 2 = 1 2 m ∑ i = 0 m − 1 ( w ⃗ ⋅ x ⃗ i + b − y i ) 2 J(\vec w, b) = \frac 1 {2m} \sum_{i = 0}^{m-1} (f_{w, b}(\vec x_i) - y_i)^2 \newline = \frac 1 {2m} \sum_{i = 0}^{m-1} (\vec w \cdot \vec x_i + b - y_i)^2 J(w ,b)=2m1i=0m1(fw,b(x i)yi)2=2m1i=0m1(w x i+byi)2
其中, x ⃗ i \vec x_i x i代表第i行数据,是一个向量。

在代码中的实现(使用loop)

# X: 2darray, 存放数据的2D数组,每行为一个数据
# y: 1darray, 存放target,是一个1D数组
import numpy as np
def dompute_cost(X, y, w, b):
    cost = 0.0f
	for i in range(X.shape[0]): # 遍历每个数据
    	f_wb = np.dot(X[i], w) + b
    	cost = cost + (f_wb - y[i])**2
	cost /= (2*m)

Compute Gradient

梯度的严格定义是多元函数的一阶导数。设 n n n元函数 f ( x ) f(x) f(x)对自变量 x = ( x 1 , x 2 , . . . , x n ) T x = (x_1, x_2, ..., x_n)^T x=(x1,x2,...,xn)T各分量 x i x_i xi的偏导数 ∂ f ( x ) ∂ x i \frac {\partial f(x)}{\partial x_i} xif(x)都存在,称函数 f ( x ) f(x) f(x) x x x一阶可导,向量
∇ f ( x ) = [ ∂ f ( x ) ∂ x 1 ∂ f ( x ) ∂ x 2 ⋮ ∂ f ( x ) ∂ x n ] \nabla f(x) = \left[\begin{array}{} \frac {\partial f(x)}{\partial x_1}\\ \frac {\partial f(x)}{\partial x_2}\\ \vdots\\ \frac {\partial f(x)}{\partial x_n} \end{array}\right] f(x)= x1f(x)x2f(x)xnf(x)
f ( x ) f(x) f(x)对x的一阶导数或梯度。

在多元线性回归中,梯度的计算可以表示为
∂ J ( w ⃗ , b ) ∂ w j = 1 m ∑ i = 0 m − 1 ( f w , b ( x ⃗ i ) − y i ) x i , j ∂ J ( w ⃗ , b ) ∂ b = 1 m ∑ i = 0 m − 1 ( f w , b ( x ⃗ i ) − y i ) \frac {\partial J(\vec w, b)}{\partial w_j} = \frac 1 m \sum_{i = 0}^{m-1} (f_{w, b}(\vec x_i) - y_i)x_{i, j} \newline \frac {\partial J(\vec w, b)}{\partial b} = \frac 1 m \sum_{i = 0}^{m-1} (f_{w, b}(\vec x_i) - y_i) wjJ(w ,b)=m1i=0m1(fw,b(x i)yi)xi,jbJ(w ,b)=m1i=0m1(fw,b(x i)yi)
其中 e r r o r = ( f w , b ( x ⃗ i ) − y i ) error = (f_{w, b}(\vec x_i) - y_i) error=(fw,b(x i)yi)是公共项,可以在计算每个样例的时候复用。

代码实现如下

def compute_gradient(X, y, w, b):
    m, n = X.shape
    dj_dw = np.zeros((n,))
    dj_db = 0.0f
    
    for i in range(m):
        err = np.dot(X[i], w) + b - y[i]
        # 累加每个dj_jw
        for j in range(n):
            dj_dw += err * x[i, j]
        # 累加dj_db
        dj_db += err
    # 求均值
    dj_dw /= m
    dj_db /= m
    return dj_db, dj_dw

Gradient Descent

多元梯度下降的数学表达式可以表示为
w j = w j − α ∂ J ( w ⃗ , b ) ∂ w j , j = 0 , … , n − 1 b = b − α ∂ J ( w ⃗ , b ) ∂ b w_j = w_j - \alpha \frac {\partial J(\vec w, b)}{\partial w_j}, j = 0, \dots, n-1 \newline b = b - \alpha \frac {\partial J(\vec w, b)}{\partial b} wj=wjαwjJ(w ,b),j=0,,n1b=bαbJ(w ,b)
迭代上式直到拟合。

代码实现

def gradient_descent(X, y, w_init, b_init, cost_function, gradient_function, alpha, num_iter):
    w = copy.deepcopy(w_in) # 深拷贝w_in,避免修改原始变量
    b = b_in
    
    for i in range(num_iters):
    	dj_db, dj_dw = gradient_funciton(X, y, w, b)
   		w -= alpha * dj_dw
    	b -= alpha * dj_db
    return w, b

Feature Scaling

Background

在上一个lab中,我们尝试实现了多元梯度下降,但模型的训练结果并不理想,经过1000轮迭代后,绘制cost曲线如下

在这里插入图片描述

模型远没有拟合,且预测值与目标值差异很大。这是由于特征尺度不同所导致的。

对多元梯度下降,不同的特征会有不同的取值范围,例如在房屋价格预测模型中,房屋的面积范围可能是[200, 3000],而房屋的卧室数范围可能是[0, 5],两者的度量尺度存在很大的差异。

在这里插入图片描述

如图所示,当两个特征之间的度量尺度存在很大差异的时候,会导致两者cost函数的等高线图呈椭圆形(contour);在图中的例子里,房屋大小对应的参数 w 1 w_1 w1只需要轻微变化,就能很大程度影响预测值,而卧室数量对应的参数 w 2 w_2 w2则需要移动很大的步长才能对预测值产生明显影响。最终导致梯度下降在椭圆较瘦的方向上反复跳动,降低学习效率。

避免这种问题,可以采用特征缩放,调整(rescale)不同特征的尺度。例如全都缩放到[0, 1],使得两者在可以相互比较的度量尺度(take on comparable range of values)内,等高线图呈近乎圆形。

在这里插入图片描述

总结一下,之所以使用特征缩放,原因有两个:

  1. 防止范围过广的特征左右误差(距离)
  2. 加速梯度下降的收敛

下图是从数学表达式上理解为什么需要缩放特征,因为w参数的更新量需要乘对应的特征值,因此在每个参数的 α \alpha α相同的情况下,特征值较大的参数变化就更大。例如,房屋大小的特征值普遍大于1000,而床位数通常在2-4之间,房屋大小的更新速度要比床位数快得多。

在这里插入图片描述

Methods

实现特征缩放有很多方法,例如**除阈值(Devide range),均值标准化(Mean normalization)Z-score 标准化(Z-score normalization)**等。

当数据存在下面几种特点的时候,应当进行缩放:

  • 值域太大,例如[-1000, 1000]
  • 值域太小,例如[-0.001, 0.001]
  • 基太大,例如[900, 1000]

缩放后的数据范围应当接近[-1, 1]或[0, 1]。

Devide range

数学表达式如下
x j , s c a l e d = x j max ⁡ − min ⁡ x_{j, scaled} = \frac {x_j} {\max - \min} xj,scaled=maxminxj

Mean normalization

首先计算均值 μ j \mu_j μj,然后用误差除值域
x j = x j − μ j max ⁡ − min ⁡ x_j = \frac {x_j - \mu_j}{\max - \min} xj=maxminxjμj

Z-score normalization

计算标准分数(Standard Score),又称z-score,经过该变换的样本值通常在 ± 5 \pm5 ±5 ± 6 \pm 6 ±6之间。

标准分数的计算如下
z = x − μ σ z = \frac {x - \mu} \sigma z=σxμ
其中, μ \mu μ为均值, σ \sigma σ为标准差。

这里其实有个挺细节的点:计算Z值需要的是总体的均值和标准差,而不是样本均值和标准差。而总体通常是没有办法度量的,因此通常会使用随机样本来评估,此时标准分数为
z = x − x ˉ S z = \frac {x - \bar x} {S} z=Sxxˉ

另一个需要关注的点为,这里将特征值标准化为了标准正态分布,处理后的特征值,均值为0,标准差为1

向量标准化

将特征向量整体变为单位向量(即除以向量长度)
x = x ∣ ∣ x ∣ ∣ x = \frac x {||x||} x=∣∣x∣∣x

Implement

实现一下z-score标准化。

首先计算均值和标准差
μ j = 1 m ∑ i = 0 m − 1 x j ( i ) σ j 2 = 1 m ∑ i = 0 m − 1 ( x j ( i ) − μ j ) 2 \mu_j = \frac 1 m \sum_{i = 0}^{m - 1} x_j^{(i)} \newline \sigma_j^2 = \frac 1 m \sum_{i = 0}^{m - 1} (x_j^{(i)} - \mu_j)^2 μj=m1i=0m1xj(i)σj2=m1i=0m1(xj(i)μj)2
使用这两个测量值缩放输入
x j ( i ) = x j ( i ) − μ j σ j x_j^{(i)} = \frac {x_j^{(i)} - \mu_j} {\sigma_j} xj(i)=σjxj(i)μj

注意:应当把均值和方差也一并存储下来,后面预测新值的时候再次用来标准化。

NumPy提供了计算这些测量值的方法,可以直接调用

import numpy as np

def zscore_normalize(X):
    '''
    Compute zscore normalized X
    Args:
    	X (ndarray): Shape(m, n) input data, m examples, n features
    	
   	Returns:
   		X_norm (ndarray): Shape(m, n) normalized X
   		mu (ndarray): Shape(n, ) 	  mean of each feature
   		sigma (ndarray): Shape(n,) 	  standard deviation of each feature
    '''
    mu = np.mean(X, axis = 0) # compute mean of each column
    sigma = np.std(X, axis = 0) # compute standard deviation of each column
    X_norm = (X - mu) / sigma # will be broadcast to each column
    
    return (X_norm, mu, sigma)

标准化的结果可以通过绘制散点图来观察

在这里插入图片描述

可以观察到,减去均值后,特征值会分布在0的周围;而除以标准差后,特征值会被缩放到较小的范围。

也可以通过np.ptp()来观察极差的变化,ptp是peak to peak的缩写,即从一个极值到另一个极值。

>>> # normalize the original features
>>> X_norm, X_mu, X_sigma = zscore_normalize_features(X_train)
>>> print(f"X_mu = {X_mu}, \nX_sigma = {X_sigma}")
>>> print(f"Peak to Peak range by column in Raw        X:{np.ptp(X_train,axis=0)}")   
>>> print(f"Peak to Peak range by column in Normalized X:{np.ptp(X_norm,axis=0)}")
X_mu = [1.42e+03 2.72e+00 1.38e+00 3.84e+01], 
X_sigma = [411.62   0.65   0.49  25.78]
Peak to Peak range by column in Raw        X:[2.41e+03 4.00e+00 1.00e+00 9.50e+01]
Peak to Peak range by column in Normalized X:[5.85 6.14 2.06 3.69]

Cost Contour

通过绘制成本函数的等高线图,也可以观察到不同特征的阈值是否匹配

在这里插入图片描述

如上图所示,在对数据进行标准化之前,特征值之间的阈值差异过大,甚至无法观察出图像是一个椭圆;而在进行标准化后,两个特征值之间的阈值更为接近,这会使得梯度下降时的步长更相似。

Checking Convergence

通过学习分辨什么是运行良好的梯度下降,从而选择更好的学习率。

  • 绘制迭代次数(iterations)/损失函数 J ( w ⃗ , b ) J(\vec w, b) J(w ,b)图像,是**学习曲线(Learning curve)**的一种;如果梯度下降运行正常,损失函数应当一直下降;若出现上升,可能是因为bug或者 α \alpha α太大;同时,曲线应当逐渐变平(逐渐收敛),通过图像可以很方便地找到拟合位置,确定何时完成训练

在这里插入图片描述

  • 使用自动收敛判断(Autimatic convergence test)。令 ε \varepsilon ε为一个极小值,例如 1 0 − 3 10^{-3} 103,若某一次梯度下降中成本的减少量 ≤ ε \le \varepsilon ε,则认为模型收敛。当然,这种方法要求选择合适的 ε \varepsilon ε,且不如画图直观。

Choosing the Learning Rate

在这里插入图片描述

如图所示,当损失函数图像出现上下抖动,或者成本不断增高的时候(本质上是成本没有下降);可能是由于代码编写错误或者学习率过高导致的。区分究竟是 α \alpha α的问题,还是代码编写错了,可以先将 α \alpha α设为一个极小值,看看修改后损失函数会不会正常下降。如果不会的话,通常是代码编写错了。

注意:这里设一个非常非常小的 α \alpha α是为了辨别错误类型。过小的 α \alpha α也会是梯度下降过慢,增加迭代开销。

在开始训练一个新的模型的时候,可以先设置一组不同量级的学习率,例如alphas = [0.001, 0.01, 0.1, 1],使用每个学习率运行少量迭代数,并绘制成本函数。从而选择快速且始终保持下降的学习率。重点是要找到 α \alpha α过小和过大的边界,如图所示;并选择尽可能大的学习率。

在这里插入图片描述

为了能够直观观察是否收敛,我们可以像前面所学的那样,绘制“成本函数-迭代次数”的变化曲线,以及在“成本-参数”曲线中,标注梯度下降点的位置

import numpy as np
import matplotlib.pyplot as plt

def plot_cost(X, y, hist):
    '''
    plot cost-iteration and cost-w[0] fig
    
    Arguments:
    	X: np.ndarray: train examples in 2D array
    	y: np.ndarray: target values in 1D array
    	hist: dict: dict of iteration history
    	hist["cost"] = []; hist["params"] = []; hist["grads"]=[]; hist["iter"]=[];
    
    Returns: nothing
    '''
    ws = np.array(p[0] for p in hist["params"]) # get w[0]s
    rng = max(abs(ws[:, 0].min()), abs(ws[:,0].max())) # get range of ws
    wr = np.linspace(-rng+0.27, rng+0.27, 20) # x axis for plot
    cst = [compute_cost(X, y, np.array([wr[i], -32, -67, -1.46]), 221) for i in range(len(wr))] # compute cost of in wr range, with fixed parameters w_1, w_2, w_3
    
    fig, ax = plt.subplots(1, 2, figsize = (12, 3))
    ax[0].plot(hist["iter"], hist["cost"]); 
    ax[0].set_title("Cost vs Iteration")
    ax[0].set_xlabel("iteration"); ax[0].set_ylabel("Cost") # plot cost vs iteration
    
    ax[1].plot(wr, cst); ax[1].set_title("Cost vs w[0]") # plot gradient descent direction
    ax[1].set_xlabel("w[0]"); ax[1].set_ylabel("Cost")
    ax[1].plot(ws[:0], hist["Cost"]) # plot cost vs w[0] bowl curve

因为这个函数只关注了4个特征中的一个,而其他的3个特征取了最优固定值,因此绘制出来的图像可能会有些错位。

下面是一些不同学习率下的绘图结果

α = 9.9 e − 7 \alpha = 9.9e-7 α=9.9e7

在这里插入图片描述

成本不降反增,且参数会越过(overshoot)最优值,不断上升。

α = 9 e − 7 \alpha = 9e-7 α=9e7

在这里插入图片描述

成本能够正常下降,但参数仍然在震荡,虽然最终可以收敛,但效率会很差

α = 1 e − 7 \alpha = 1e-7 α=1e7

在这里插入图片描述

成本能够正常下降,且参数不会越过最小值,这样收敛速度会快得多

Feature Engineering - Choosing right features

特征设计(Feature Engineering)就是根据对模型的理解和直觉,设计新的特征,通常使用转换(transform)或者组合(combine)原始特征的方法。进行特征设计的目的是获得更利于模型做出准确预测的特征。

以预测房价为例,房屋可以有长和宽两个特征,你可以选择将两个特征分别作为特征值分配参数
f w ⃗ , b ( x ⃗ ) = w 1 x 1 + w 2 x 2 + b f_{\vec w, b}(\vec x) = w_1x_1 + w_2x_2 + b fw ,b(x )=w1x1+w2x2+b
也可以考虑使用长和宽计算出房屋的面积,使用面积作为特征值
x 3 = x 1 x 2 f w ⃗ , b ( x ⃗ ) = w 3 x 3 + b x_3 = x_1 x_2 \newline f_{\vec w, b}(\vec x) = w_3x_3 + b x3=x1x2fw ,b(x )=w3x3+b
直觉上来说,后者更符合现实生活中的购房情况,更利于训练准确的模型。

Polynomial regression

在某些情况下,线性模型可能不能完美地拟合我们的数据,例如下图的情况。

在这里插入图片描述

也许更高阶的函数能够更好地拟合这个数据(这部分有点数字信号处理的内容了,总之更高阶的函数能够更好拟合复杂图像).

需要注意的是,当使用更高阶的函数时,特征的缩放就显得更为重要,因为高阶函数会进一步放大不同特征值之间的极差差距,导致下降步长不一致。

Simple quadratic

首先考虑一个简单的非线性场景,设 y = 1 + x 2 y = 1 + x^2 y=1+x2

使用前面的线性回归模型进行拟合

# create target data
x = np.arange(0, 20, 1)
y = 1 + x**2
X = x.reshape(-1, 1)

model_w,model_b = run_gradient_descent_feng(X,y,iterations=1000, alpha = 1e-2)

plt.scatter(x, y, marker='x', c='r', label="Actual Value"); plt.title("no feature engineering")
plt.plot(x,X@model_w + model_b, label="Predicted Value");  plt.xlabel("X"); plt.ylabel("y"); plt.legend(); plt.show()

w,b found by gradient descent: w: [18.7], b: -52.0834

在这里插入图片描述

很明显,函数不能很好地拟合,我们需要设计更为有效的特征值,例如尝试将x取平方

# create target data
x = np.arange(0, 20, 1)
y = 1 + x**2

# Engineer features 
X = x**2      #<-- added engineered feature
X = X.reshape(-1, 1)  #X should be a 2-D Matrix
model_w,model_b = run_gradient_descent_feng(X, y, iterations=10000, alpha = 1e-5)

plt.scatter(x, y, marker='x', c='r', label="Actual Value"); plt.title("Added x**2 feature")
plt.plot(x, np.dot(X,model_w) + model_b, label="Predicted Value"); plt.xlabel("x"); plt.ylabel("y"); plt.legend(); plt.show()

w,b found by gradient descent: w: [1.], b: 0.0490

在这里插入图片描述

这一次,获得了十分接近target的拟合: y = 0.0490 + x 2 y = 0.0490 + x^2 y=0.0490+x2

Selecting Features

在前面,我们讨论了一个简单的非线性模型的例子,接下来,我们要讨论如何选择合适的特征,因为很多时候,我们不能直观推断怎样的多项式特征更符合数据。

选择特征的一种方法是利用梯度下降对权重的分配,根据权重的大小,可以判断出该特征对拟合的影响程度,从而选择合适的特征。

我们以 y = w 0 x 0 + w 1 x 1 2 + w 2 x 2 3 + b y = w_0x_0 + w_1 x_1^2 + w_2 x_2^3 + b y=w0x0+w1x12+w2x23+b和上面的数据集为例,分析使用梯度下降法选取特征的细节。

Feature scaling

在进行梯度下降之前,我们需要先进行特征缩放,因为 x 0 , x 1 2 x_0, x_1^2 x0,x12 x 2 3 x_2^3 x23明显具有不同的阈值,我们可以进行比较:

# create target data
x = np.arange(0,20,1)
X = np.c_[x, x**2, x**3]
print(f"Peak to Peak range by column in Raw        X:{np.ptp(X,axis=0)}")

# add mean_normalization 
X = zscore_normalize_features(X)     
print(f"Peak to Peak range by column in Normalized X:{np.ptp(X,axis=0)}")

Peak to Peak range by column in Raw X:[ 19 361 6859]
Peak to Peak range by column in Normalized X:[3.3 3.18 3.28]

进行缩放后,特征阈值更加一致,梯度下降的效果会更好。

np.c_[..]用来将不同的列串联,或者按照NumPy手册的说法,根据第二个轴(along the second axis)。

这个方法是np.r_['-1, 2, 0', index expression]的快捷写法,numpy.r_方法可以将切片对象转换为沿第一个轴的串联(c和r分别是column和row的简写)。两者都是用于快速构建数组的方法。在底层,它们都调用self.concatenate(tuple(objs), axis = axis)

所谓”串联“,实际上是将用逗号隔开的元素,按照指定的坐标轴,合并到同一个数组内,例如

>>> np.r_[np.array([1,2,3]), 0, 0, np.array([4,5,6])]
array([1, 2, 3, ..., 4, 5, 6])
>>> np.c_[np.array([1,2,3]), np.array([4,5,6])]
array([[1, 4],
    [2, 5],
    [3, 6]])

需要注意的是,串联的对应轴必须等长,例如将第二个改为

>>> np.c_[np.array([1,2,3]),0 , np.array([4,5,6])] # 错误的写法,一定会报错
'''ValueError: all the input array dimensions except for the concatenation axis must match exactly'''

np.r_可以通过指定string integers来指定串联效果,这个字符串由三个逗号分隔的整数组成,分别指示要连接的轴,强制将输入转换的最小维度(即维度要大于等于该值,但这个值必须大于等于连接后的维度),以及哪个轴将包含数组的起始(这个数字必须低于维度);例如

>>> np.r_['0,2,0', [1,2,3], [4,5,6]]
array([[1],
    [2],
    [3],
    [4],
    [5],
    [6]])
>>> np.r_['0,1,0', [1,2,3], [4,5,6]]
array([1, 2, 3, 4, 5, 6])
>>> np.r_['1,2,0', [1,2,3], [4,5,6]]
array([[1, 4],
    [2, 5],
    [3, 6]])
>>> np.r_['1,2,1', [1,2,3], [4,5,6]]
array([[1, 2, 3, 4, 5, 6]])

Gradient descent with non-linear feature

使用缩放后的特征,执行梯度下降。因为已经经过了特征缩放,所以alpha可以设大一些。

x = np.arange(0,20,1)
y = x**2

X = np.c_[x, x**2, x**3]
X = zscore_normalize_features(X) 

model_w, model_b = run_gradient_descent_feng(X, y, iterations=100000, alpha=1e-1)

plt.scatter(x, y, marker='x', c='r', label="Actual Value"); plt.title("Normalized x x**2, x**3 feature")
plt.plot(x,X@model_w + model_b, label="Predicted Value"); plt.xlabel("x"); plt.ylabel("y"); plt.legend(); plt.show()

@做的是矩阵乘法运算

w,b found by gradient descent: w: [5.27e-05 1.13e+02 8.43e-05], b: 123.5000

在这里插入图片描述

从权重的结果来看, x 2 x^2 x2的影响比其他两个特征大得多,可以视为其他两个特征被淘汰。

An Alternate View

从另一个视角来看特征选取,虽然我们使用了更高阶的特征,但抛开特征设计,我们使用的梯度下降方法仍然是线性回归的梯度下降方法,因此,当一个经过设计的特征适合数据的时候,该特征应当与目标值线性相关

# create target data
x = np.arange(0, 20, 1)
y = x**2

# engineer features .
X = np.c_[x, x**2, x**3]   #<-- added engineered feature
X_features = ['x','x^2','x^3']

fig,ax=plt.subplots(1, 3, figsize=(12, 3), sharey=True)
for i in range(len(ax)):
    ax[i].scatter(X[:,i],y)
    ax[i].set_xlabel(X_features[i])
ax[0].set_ylabel("y")
plt.show()

在这里插入图片描述

如图所示,可见特征 x 2 x^2 x2与目标值线性相关,因此应当选择该特征。

西瓜书内容补充

多元闭式解

已知在一元线性回归中,有
f ( x i ) = w x i + b ( w ∗ , b ∗ ) = arg ⁡ min ⁡ ( w , b ) ∑ i = 1 m ( f ( x i ) − y i ) 2 f(x_i) = w x_i + b\newline (w^*, b^*) = \arg \min_{(w, b)} \sum_{i = 1}^m (f(x_i) - y_i)^2 f(xi)=wxi+b(w,b)=arg(w,b)mini=1m(f(xi)yi)2
w ∗ , b ∗ w^*, b^* w,b w , b w, b w,b的解,对于多元线性回归,我们试图获得
f ( x ⃗ i ) = w ⃗ T x ⃗ i + b ,使得 f ( x ⃗ i ) ≃ y i f(\vec x_i) = \vec w^T \vec x_i + b,使得f(\vec x_i)\simeq y_i f(x i)=w Tx i+b,使得f(x i)yi
类似的,我们可以写出解的形式
( w ⃗ ∗ , b ∗ ) = arg ⁡ min ⁡ ( w ⃗ , b ) ∑ i = 1 m ( f ( x ⃗ i ) − y i ) 2 = arg ⁡ min ⁡ ( w ⃗ , b ) ∑ i = 1 m ( w ⃗ T x ⃗ i + b − y i ) 2 (\vec w^*, b^*) = \arg \min_{(\vec w, b)} \sum_{i = 1}^m (f(\vec x_i) - y_i)^2 \newline = \arg \min_{(\vec w, b)} \sum_{i = 1}^m (\vec w^T \vec x_i + b - y_i)^2 (w ,b)=arg(w ,b)mini=1m(f(x i)yi)2=arg(w ,b)mini=1m(w Tx i+byi)2
为了使计算更加简便,可以令
w ⃗ ^ = ( w ⃗ ; b ) = ( w 1 , w 2 , … , w n , b ) 对应的 , X = ( x 11 x 12 ⋯ x 1 n 1 x 21 x 22 ⋯ x 2 n 1 ⋮ ⋮ ⋱ ⋮ ⋮ x m 1 x m 2 ⋯ x m n 1 ) \hat {\vec w} = (\vec w; b) = (w_1, w_2, \dots, w_n, b) \newline 对应的, X = \left( \begin{array}{} x_{11} & x_{12} & \cdots & x_{1n} & 1 \\ x_{21} & x_{22} & \cdots & x_{2n} & 1 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ x_{m1} & x_{m2} & \cdots & x_{mn} & 1 \end{array} \right) w ^=(w ;b)=(w1,w2,,wn,b)对应的,X= x11x21xm1x12x22xm2x1nx2nxmn111
则有
w ⃗ ^ ∗ = arg ⁡ min ⁡ w ⃗ ^ ( X w ⃗ ^ − y ⃗ ) T ( X w ⃗ ^ − y ⃗ ) \hat {\vec w}^* = \arg\min_{\hat {\vec w}} ( X \hat {\vec w} - \vec y)^T( X \hat {\vec w} - \vec y) w ^=argw ^min(Xw ^y )T(Xw ^y )
E w ⃗ ^ = ( X w ⃗ ^ − y ⃗ ) T ( X w ⃗ ^ − y ⃗ ) E_{\hat {\vec w}} = ( X \hat {\vec w} - \vec y)^T( X \hat {\vec w} - \vec y) Ew ^=(Xw ^y )T(Xw ^y ),则对 w ⃗ ^ \hat {\vec w} w ^求导

∂ E w ⃗ ^ ∂ w ⃗ ^ = ∂ y ⃗ T y ⃗ ∂ w ⃗ ^ − ∂ y ⃗ T X w ⃗ ^ ∂ w ⃗ ^ − ∂ w ⃗ ^ T X T y ⃗ ∂ w ⃗ ^ + ∂ w ⃗ ^ T X T X w ⃗ ^ ∂ w ⃗ ^ ∵ ∂ α T x ⃗ ∂ x ⃗ = ∂ x ⃗ T α ∂ x ⃗ = α ∂ x ⃗ T A x ⃗ ∂ x ⃗ = ( A + A T ) x ⃗ ∴ ∂ E w ⃗ ^ ∂ w ⃗ ^ = 0 − X T y ⃗ − X T y ⃗ + ( X T X + X T X ) w ⃗ ^ = 2 X T ( X w ⃗ ^ − y ⃗ ) \frac {\partial E_{\hat {\vec w}}}{\partial \hat {\vec w}} = \frac {\partial \vec y^T \vec y}{\partial \hat {\vec w}} - \frac {\partial \vec y^T X \hat {\vec w}}{\partial \hat {\vec w}} - \frac {\partial \hat {\vec w}^T X^T \vec y }{\partial \hat {\vec w}} + \frac {\partial \hat {\vec w}^T X^T X \hat {\vec w} }{\partial \hat {\vec w}} \newline \because \frac {\partial \alpha^T \vec x}{\partial \vec x} = \frac {\partial \vec x^T \alpha}{\partial \vec x} = \alpha \newline \frac {\partial \vec x^T A \vec x}{\partial \vec x} = ( A + A^T)\vec x \newline \therefore \frac {\partial E_{\hat {\vec w}}}{\partial \hat {\vec w}} = 0 - X^T \vec y - X^T \vec y + ( X^T X + X^T X)\hat {\vec w} \newline = 2 X^T( X \hat {\vec w} - \vec y) w ^Ew ^=w ^y Ty w ^y TXw ^w ^w ^TXTy +w ^w ^TXTXw ^x αTx =x x Tα=αx x TAx =(A+AT)x w ^Ew ^=0XTy XTy +(XTX+XTX)w ^=2XT(Xw ^y )
令该式为0,可得 w ⃗ ^ ∗ \hat {\vec w}^* w ^的闭式解。

广义线性模型(generalized linear model)

通常的线性模型可以简写为
y = w ⃗ T x ⃗ + b y = \vec w^T \vec x + b y=w Tx +b
如果让线性模型逼近y的衍生函数,例如,让y呈指数变化,可以写为
ln ⁡ y = w ⃗ T x ⃗ + b \ln y = \vec w^T \vec x + b lny=w Tx +b
即让 e w ⃗ T x ⃗ + b e^{\vec w^T \vec x + b} ew Tx +b逼近 y y y,上面这种回归称为对数线性回归(log-linear regression),类似这样的,考虑单调可微的联系函数(link function) g ( ⋅ ) g(\cdot) g(),使得
y = g − 1 ( w ⃗ T x ⃗ + b ) y = g^{-1}(\vec w^T \vec x + b) y=g1(w Tx +b)
这样的模型称为广义线性模型(generalized linear model)

或者按照吴恩达老师所讲的理解思路,广义线性模型就是我们试图寻找一个 g ( ⋅ ) g(\cdot) g(),使得 y y y g ( w ⃗ T x ⃗ + b ) g(\vec w^T \vec x + b) g(w Tx +b)线性相关。

参考

  • 2022吴恩达机器学习
  • 《机器学习》周志华(西瓜书)
  • 南瓜书
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Recitative

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值