本节导图:https://www.processon.com/view/link/5fd9f5dc5653bb06f344d655
大纲
1. 数值计算库SciPy
SciPy在Numpy的基础上,增加了众多的数学计算、科学计算、以及工程计算中的常用模块,例如:线性代数、方程求解、信号处理、统计学、图像处理、稀疏矩阵等。
接下来我们主要介绍其中的一些重要的子模块,它们在我们后续的NLP生涯中很重要。
2. 拟合与优化
scipy.optimize 模块提供了许多数值优化算法,我们可以使用其进行最小二乘拟合和计算函数最小值
最小二乘拟合
最小二乘法是一种数学优化技术,它通过最小化平方误差,来寻求与数据最佳匹配的函数。
如果用p 来表示函数中需要确定的参数,目标是找到一组p 使得函数S 的值最小,于是最小二乘拟合被表示为:
机器学习里线性回归模型的参数估计使用的就是最小二乘法。
线性回归模型,即使用线性模型解决实值预测问题的模型。
这里,通过一个例子,使用optimize.leastsq() 函数对二维数据点进行最小二乘拟合,并计算出直线的参数(斜率和截距)。
import numpy as np
from scipy import optimize
X = np.array([8.19, 2.72, 6.39, 8.71, 4.7, 2.66, 3.78])
Y = np.array([7.01, 2.78, 6.47, 6.71, 4.1, 4.23, 4.05])
def residuals(p):
"""计算以p为参数的直线和原始数据之间的误差"""
k, b = p
return Y - (k * X + b)
r = optimize.leastsq(residuals, [1, 0])
k, b = r[0]
print('斜率k={}, 截距b={}'.format(k, b))
斜率k=0.6134953491930442, 截距b=1.794092543259387
计算函数极值
optimize库还提供了许多求函数最小值的算法,比如:Nelder-Mead、Powell、CG、BFGS、Newton-CG、L-BFGS-B等。
这里使用BFGS算法和CG算法求解Rosenbrock函数的最小值**。
Rosenbrock函数经常被用来测试最小化算法的收敛速度,
-
函数公式如下: -
函数图像如下:
此函数的特点是:有一个平坦的山谷区域,收敛到山谷区域较为容易,但在山谷区域搜索到最小点则比较困难。
结合公式和图像,我们易知,最小值发生在(1,1) 处,最小值是0
一些优化算法需要指定fprime 参数,即给出目标函数的偏导函数。f(x,y) 对x , y 的偏导函数为:
import numpy as np
from scipy import optimize
def target_func(p):
"""目标函数,rosenbrock函数"""
x, y = p
z = (1 - x) ** 2 + 100 * (y - x ** 2) ** 2
return z
def prime_func(p):
"""目标函数的导函数"""
x, y = p
dx = -2 + 2 * x - 400 * x * (y - x ** 2)
dy = 200 * y - 200 * x ** 2
return np.array([dx, dy])
init_point = np.array([0, 2])
result1 = optimize.fmin_bfgs(target_func, init_point, prime_func)
print(result1)
result2 = optimize.fmin_cg(target_func, init_point, prime_func)
print(result2)
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 23
Function evaluations: 30
Gradient evaluations: 30
[0.99999989 0.99999977]
Optimization terminated successfully.
Current function value: 0.000000
Iterations: 19
Function evaluations: 43
Gradient evaluations: 43
[1.00000005 1.00000009]
3. 线性代数和矩阵分解
Numpy和SciPy都提供了线性代数库 linalg ,但SciPy的线性代数库比Numpy更加全面。
这里,我们主要演示计算矩阵的特征值和特征向量以及矩阵分解
计算矩阵的特征值和特征向量
n*n 的矩阵A 可以看做一个n维空间的线性变换。如果x 为n维空间的一个向量,那Ax (A 与x 的矩阵乘积)就是对x 进行了线性变换,结果是一个新的向量。
如果x 为变换矩阵A 的特征向量,那么经过Ax 的线性变换后,新向量仍与x 方向相同,但长度可能发生变换,长度的缩放值由特征值λ 决定。
所以,特征向量和特征值有如下公式:
以二维平面上的线性变换矩阵为例。
import numpy as np
from scipy import linalg
A = np.array([[1, -0.3], [-0.1, 0.9]])
evalues, evectors = linalg.eig(A)
print(evalues)
print(evectors)
[1.13027756+0.j 0.76972244+0.j]
[[ 0.91724574 0.79325185]
[-0.3983218 0.60889368]]
奇异值分解
奇异值分解是线性代数中一种重要的矩阵分解技术,在机器学习领域有着重要的应用。
假设X 是一个m*n 的矩阵,存在一个分解,使得:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ei96paTy-1637250312971)(C:\Users\winter\AppData\Roaming\Typora\typora-user-images\image-20211118233420758.png)]
其中,矩阵U 为m*m 维,Σ 为m*n 维的对角矩阵,矩阵V 为n*n 维,这样的分解就是奇异值分解,Σ 对角线上的元素为矩阵X 的奇异值,奇异值按照从大到小排列。
我们来对一个矩阵X 进行奇异值分解:
import numpy as np
from scipy import linalg
X = np.arange(15).reshape(3,5)
print(X, X.shape,'\n')
svd_values = linalg.svdvals(X)
print(svd_values, '\n')
U, sigma, V = linalg.svd(X)
print(U, U.shape, '\n')
print(sigma, sigma.shape, '\n')
print(V, V.shape, '\n')
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]] (3, 5)
[3.17420265e+01 2.72832424e+00 9.19513106e-16]
[[-0.15425367 -0.89974393 0.40824829]
[-0.50248417 -0.28432901 -0.81649658]
[-0.85071468 0.3310859 0.40824829]] (3, 3)
[3.17420265e+01 2.72832424e+00 9.19513106e-16] (3,)
[[-0.34716018 -0.39465093 -0.44214167 -0.48963242 -0.53712316]
[ 0.69244481 0.37980343 0.06716206 -0.24547932 -0.55812069]
[ 0.45650723 -0.61153491 0.23629065 -0.46400549 0.38274252]
[-0.2754905 -0.21468338 0.83080946 0.08439319 -0.42502878]
[-0.34015605 0.52908988 0.23221189 -0.69106924 0.26992351]] (5, 5)
?
奇异值的大小衡量一些潜在概念的重要性,我们往往需要筛选出最重要的一些奇异值,来降维重构U Σ V 三个矩阵。
keep_dims = 2
sigma2 = np.diag(sigma[:keep_dims])
U2 = U[:, :keep_dims]
V2 = V[:keep_dims, :]
print(U2.shape, sigma2.shape, V2.shape)
(3, 2) (2, 2) (2, 5)
在NLP中,潜在语义分析(LSA)就是使用SVD来进行的。此时,我们的输入矩阵A 为词-文档矩阵,每一行代表一个词,每一列代表一个文档,矩阵中的值A(i,j) 为词i 在文档j 中的频数。
如下图示:
我们将 词-文档矩阵A 通过SVD分解,并截取最重要的100个奇异值后,得到三个矩阵:X B Y
X 是降维后的词矩阵,每一行代表一个词,每一列标识了一个潜在的语义概念;Y 是降维后的文档矩阵,每一列代表一个文档,每一行标识了一个潜在的主题概念;B 标识每个语义类和每个主题类之间的相关性;
分解后,你如果需要计算 词和词之间、文档和文档 之间的相似度,就可以只计算100维,否则你需要计算的是 50万维 或 100万维。
4. 统计学分布与检验
SciPy中的stats 模块包含了多种概率分布的随机变量,这里的随机变量包括:连续随机变量 和 离散随机变量。
from scipy import stats
import numpy as np
正态分布
我们这里以正态分布为例。定义一个符合正态分布的随机变量X
X = stats.norm(loc=1.0, scale=2.0)
X.stats()
(array(1.), array(4.))#平均值和方差
接下来调用随机变量X 的rvs() 方法,得到一个多次随机采样值的数组samples
samples = X.rvs(size=1000)
samples.shape
(1000,)
接下来通过np.mean() 方法和np.var() 方法验证下
np.mean(samples), np.var(samples)
(1.038664111644651, 3.831619958926069)
我们也可以使用fit() 方法对随机取样序列samples 进行拟合,它返回正态分布的参数(均值和标准差)
stats.norm.fit(samples)
(1.038664111644651, 1.9574524154947086)
卡方分布与卡方检验
卡方分布
若n个相互独立的随机变量ξ? ,ξ? ,…,ξn ,均服从标准正态分布,则这n个服从标准正态分布的随机变量的平方和构成一新的随机变量,其分布规律称为卡方分布。
卡方检验
可以通过卡方分布来进行独立性检验,即:判断观测值与理论值的差异是否只是因为随机误差造成的。独立性检验,通俗点说就是判断是否无关。
我们假设有2个袋子,有5个球,每个袋子中球的分布不同。现在要判断两个袋子中的球是否是平均分布的;
def choose_balls(probs, size):
"""设置当前袋子里球的分布为probs,然后取size次,返回每个球取到的次数"""
n_ball = len(probs)
rv = stats.rv_discrete(values=(range(n_ball), probs))
samples = rv.rvs(size=size)
counts = np.bincount(samples)
return counts
counts1 = choose_balls([0.18, 0.24, 0.25, 0.16, 0.17], 500)
counts2 = choose_balls([0.2] * 5, 500)
np.random.seed(42)
chi1, p1 = stats.chisquare(counts1)
print("counts =", counts1, "chi1 =", chi1, "p1 =", p1)
chi2, p2 = stats.chisquare(counts2)
print("counts =", counts2, "chi2 =", chi2, "p2 =", p2)
counts = [ 95 106 137 80 82] chi1 = 21.54 p1 = 0.00024741435438580667
counts = [ 99 91 101 111 98] chi2 = 2.08 p2 = 0.7210475511959114
卡方检验的零假设为样本符合目标概率,由上面的检验结果可知:
- 第一个袋子对应的p值只有
0.017 ,也就是说如果第一个袋子中的球真的符合平均分布,那么得到的观测结果[103 109 122 78 88] 的概率只有0.017 , 因此可以推翻零假设,即袋子中的球不太可能是平均分布的。 - 反之,第二个袋子均匀分布下,得到观测
[111 103 106 87 93] 的概率为0.428 ,因此不能推翻零假设,即不能否定袋子中的球是均匀分布的;
我们可以将其概率密度函数画出来:
chil 和chi2 对应的位置分别为χ^2_1 和χ^2_2 ;p1 和p2 分别对应χ^2_1 右侧的面积和χ^2_2 右侧的面积
在机器学习中,我们常常要做特征选择,即删除一些不重要的、与类别无关的特征,我们可以使用卡方检验来选择特征。
我们使用卡方统计度量 词特征和类别 独立性的缺乏程度,卡方越大,独立性越小,相关性越大,特征越重要。以下是邮件二分类(是否垃圾)任务下,特征词**“赌博”**出现的文档数情况:
table = [
[40, 10],
[60, 90]
]
chi2, p, _, _ = stats.chi2_contingency(table)
print(chi2, p)
22.42666666666667 2.1832165337148537e-06
5. 稀疏矩阵
为什么要用稀疏矩阵
在机器学习建模时,经常会出现许多大型的矩阵,这些矩阵中大部分元素都为0,这就是稀疏矩阵。用数组直接存储稀疏矩阵非常浪费,由于矩阵的稀疏性,我们可以只保存其非零元素,从而节约内存。
在传统的向量空间模型中,我们将每个文档表示成一个固定顺序排列的词向量,向量中的每个值 是当前词在当前文档中的词频。如下图:
这个矩阵是稀疏的,因为它中的很多元素都为0,即词频为0。词库中的词往往有数十万,而文档数也可能成千上万,如果不用稀疏矩阵存储,将会造成巨大的内存浪费,甚至,你的机器会因为内存不够而退出。
常用的几种稀疏矩阵
scipy.sparse 中提供了许多稀疏矩阵的格式,每种格式都有不同的用途。其中dok_matrix 和lil_matrix 适合逐渐添加元素。
from scipy import sparse
先看dok_matrix ,
- 它从dict继承而来,采用字典保存矩阵中的非零元素;
- 字典的key为保存
(行,列) 信息的元组;value为对应的元素值; - 显然
dok_matrix 很适合 增删改取 元素;
a = sparse.dok_matrix((10, 5))
a[1, :3] = 1.0, 2.0, 3.0
print((list(a.keys())))
print((list(a.values())))
print(a[0,0], a[1,1])
[(1, 0), (1, 1), (1, 2)]
[1.0, 2.0, 3.0]
0.0 2.0
我们再来看lil_matrix
lil_matrix 使用两个列表保存非零元素;data 保存每行中的非零元素,rows 保存非零元素所在的列;- 这种格式也很适合 增删改取 元素,并且能快速获取行相关的数据;
b = sparse.lil_matrix((10, 5))
b[0, 1] = 1
b[1, 0] = 2
b[1, 1] = 3
print(b.data, b.data.shape, '\n')
print(b.rows, b.data.shape)
[list([1.0]) list([2.0, 3.0]) list([]) list([]) list([]) list([]) list([])
list([]) list([]) list([])] (10,)
[list([1]) list([0, 1]) list([]) list([]) list([]) list([]) list([])
list([]) list([]) list([])] (10,)
最后来看coo_matrix
coo_matrix 使用三个数组row col data 来保存非零元素的信息;- 三个数组等长,
row 保存非零元素的行,col 保存非零元素的列,data 保存非零元素的值; coo_matrix 不支持元素的增删改取,一旦创建后,几乎只能转成其它矩阵去处理;- 它支持重复元素,即同一位置元素可以出现多次,转化为其它矩阵时,会将同一位置的多个值求和;
row = [2, 3, 3, 2]
col = [3, 4, 2, 3]
data = [1, 2, 3, 10]
c = sparse.coo_matrix((data, (row, col)), shape=(5, 6))
print(c.col)
print(c.row)
print(c.data, '\n')
print(c.toarray())
[3 4 2 3]
[2 3 3 2]
[ 1 2 3 10]
[[ 0 0 0 0 0 0]
[ 0 0 0 0 0 0]
[ 0 0 0 11 0 0]
[ 0 0 3 0 2 0]
[ 0 0 0 0 0 0]]
c[0, 0] = 1
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-35-c3fc3415dffd> in <module>()
----> 1 c[0, 0] = 1 # 尝试修改元素,报错
TypeError: 'coo_matrix' object does not support item assignment
c[0,0]
|