第 3 章 数据可视化


数据可视化十分重要,有助于我们理解数据。因此,本章将介绍一下
绘制图形的基本方法。

3.1 绘制二维图形
3.1.1 绘制随机图形
在绘制图形前,我们先使用 import 导入 matplotlib 的 pyplot
库,并用 plt 代表 pyplot 库。要想在 Jupyter Notebook 内显示绘
制的图形,需要加上 %matplotlib inline。首先,我们使用代码
清单 3-1-(1) 绘制一个随机图形。运行这段代码后,会显示一个图形。
In
# 代码清单 3-1-(1)
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# 创建数据
np.random.seed(1) # 固定随机数
x = np.arange(10)
y = np.random.rand(10)
# 显示图形
plt.plot(x, y) # 创建折线图
plt.show() # 绘制图形
Out
plt.plot(x, y) 用于绘制图形,plt.show() 用于显示图形。虽
然在 Jupyter Notebook 中,即使没有 plt.show(),图形也会显示出
来,但为了使代码也适用于其他编辑器,本书的代码中将保留它。
3.1.2 代码清单的格式
这里先来规定一下代码清单的编号规则。本书将以 3-1-(1)、3-1-(2)、
3-1-(3) 或 3-2-(1) 的形式给代码清单编号。第 1 个和第 2 个数字(章
号 - 序号)相同的代码清单相互关联(具有相同变量),它们将按照
第 3 个数字的顺序依次运行。也就是说,代码清单 3-1-(1) 中创建的变
量和函数有时会用在代码清单 3-1-(2) 和代码清单 3-1-(3) 中。而像 3-
2-(1) 这样序号发生改变的代码清单,则不会使用此前用过的任何变量
与函数。
此外,要想从内存中删除截至目前的历史记录,可以像下面这样写。
In
%reset
运行之后,程序会输出如下内容,按 y 键确认即可。
Out
Once deleted, variables cannot be recovered. Proceed (y/[n])?
3.1.3 绘制三次函数 f(x)=(x-2)x(x+2)
接下来,我们试着绘制 的图形。虽然我们可以很
容易地看出,在 为 -2、0、2 时, 为 0,但只有绘制了图形,才
能知道它整体上是什么形状。
首先,我们来定义函数 。
In
# 代码清单 3-2-(1)
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
def f(x):
return (x - 2) * x * (x + 2)
定义完毕后,把数值代入这个函数中的 x。运行如下代码,即可获取对
应的返回值 f。
In
# 代码清单 3-2-(2)
print(f(1))
Out
-3
即使 x 为 ndarray 数组,程序也会一次性地以 ndarray 类型返回与
各个元素对应的 f。这是因为,向量的四则运算具有对应各个元素进行
运算的性质,非常方便。
In
# 代码清单 3-2-(3)
print(f(np.array([1, 2, 3])))
Out
[-3 0 15]
3.1.4 确定绘制范围
下面,我们定义绘制图形的范围,令 x 的范围为从 -3 到 3,并定义在
此范围内计算的 x 的间隔为 0.5。
In
# 代码清单 3-2-(4)
x = np.arange(-3, 3.5, 0.5)
print(x)
Out
[-3. -2.5 -2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. 2.5 3. ]
请注意,如果写成 np.arange(-3, 3, 0.5),则输出的结果到
2.5 为止,所以这里写成了 np.arange(-3, 3.5, 0.5),代码中
的数值比 3 大。
但在定义图形中的 x 时,linspace 函数也许比 arange 更加方便。
我们可以写成 linspace(n1, n2, n),运行之后,程序将返回 n 个
在 n1 和 n2 之间等间隔分布的点。
In
# 代码清单 3-2-(5)
x =np.linspace(-3, 3, 10)
print(np.round(x, 2))
Out
[-3. -2.33 -1.67 -1. -0.33 0.33 1. 1.67 2.33 3. ]
linspace 不仅可以自然地把 n2 包含在 x 的范围内,还可以用 n 来
控制图形中线条的粗细。
print 语句中的 np.round(x, n) 是将 x 四舍五入为保留小数点后
n 位的数值的函数。
在通过 print(x) 显示向量或矩阵的情况下,小数部分有时会很长,
显得很杂乱。如果像上面这样使用 np.round(x, n),结果就会很整
洁。
3.1.5 绘制图形
接下来,让我们使用这个 x 绘制 f(x) 的图形。输出的图形应该跟如
下代码的运行结果是一样的。很简单吧?
In
# 代码清单 3-2-(6)
plt.plot(x, f(x))
plt.show()
Out
3.1.6 装饰图形
但是,我们很难通过这个图形去确认当 x 为 -2、0、2 时, 的值是
否真的为 0。另外,我们也想知道,当函数的系数发生变化时,图形会
如何变化。因此,让我们稍加调整,通过下面的代码清单 3-2-(7) 再次
绘制这个函数的图形。
In
# 代码清单3-2-(7)
# 定义函数
def f2(x, w):
return (x - w) * x * (x + 2) # (A)函数的定义
# 定义xx = np.linspace(-3, 3, 100) # (B)把x分为100份
# 绘制图形
plt.plot(x, f2(x, 2), color = 'black', label = '$w = 2$') # (C)
plt.plot(x, f2(x, 1), color = 'cornflowerblue',
label = '$w = 1$') # (D)
plt.legend(loc = "upper left") # (E)显示图例
plt.ylim(-15, 15) # (F) y轴的范围
plt.title('$f_2(x)$') # (G)标题
plt.xlabel('$x$') # (H) x标签
plt.ylabel('$y$') # (I) y标签
plt.grid(True) # (J)网格线
plt.show()
Out
图形变得很平滑,其中还加入了网格线、标签、标题和图例。如此一
来,就可以清晰地看到,函数 与 轴的交点为
-2、0、2(黑线: )。除此之外,我们还可以看到,当 ,
即 时,函数与 轴的交点为 -2、0、1(蓝线:
)。
代码清单 3-2-(7) 在开头 (A) 处定义了函数 f2(x, w)。除了变量 x
之外,这个函数的参数还有 w。改变 w 就可以改变 f2 的形状。
接下来我们定义要计算的数据点 x,这次多定义一些,把 x 分为 100
份((B))。图形是用 plt.plot 表示的,通过添加“color = '颜
色名'”,可以指定图形中线条的颜色。black 表示黑色((C)),
cornflowerblue 表示浅蓝色((D))。
我们可以通过代码清单 3-2-(8) 来查看能够使用的颜色。
In
# 代码清单3-2-(8)
import matplotlib
matplotlib.colors.cnames
Out
{'aliceblue': '#F0F8FF',
'antiquewhite': '#FAEBD7',
'aqua': '#00FFFF',
'aquamarine': '#7FFFD4',
( ……中间省略…… )
'yellowgreen': '#9ACD32'}
基本色可以仅用一个字母来指定,r 代表红色,b 代表蓝色,g 代表绿
色,c 代表蓝绿色,m 代表品红色,y 代表黄色,k 代表黑色,w 代表
白色。由于本书采用双色印刷,所以主要使用 black(黑色)、
gray(灰色)、blue(蓝色)和 cornflower blue(浅蓝色)。
此外,还可以像 color = (255, 0, 0) 这样,使用元素为 0 ~ 255
的整数的 tuple 类型的值自由地指定 RGB。
在代码清单 3-2-(7) 的 Out 中,图形左上角显示了图例,这是通过代码
清单 3-2-(7) 的 (C) 和 (D) 中的 plot 的“label = '字符串'”指定
的,(E) 中的 plt.legend() 用于显示图例。图例的位置可以自动设
定,也可以使用 loc 指定。在指定位置时,upper right 代表右上
角,upper left 代表左上角,lower left 代表左下角,lower
right 代表右下角。
轴的显示范围可以用 plt.ylim(n1, n2) 指定为从 n1 到
n2((F))。同样, 轴的范围使用 plt.xlim(n1, n2) 指定。图
形标题使用 plt.title('字符串')((G))指定。 轴与 轴的标签
分别用 plt.xlabel('字符串')((H))和 plt.ylabel('字符
串')((I))指定。plt.grid(True) 用于显示网格线((J))。我
们可以将 table 和 title 的字符串指定为用“$”括起来的 tex 形式的
表达式,这样就可以显示美观的数学式了。
3.1.7 并列显示多张图形
如果想并列显示多张图形,可以像代码清单 3-2-(9) 这样使用
plt.subplot(n1, n2, n)((C))。这样一来,就可以指定图形
的绘制位置——把一个整体分割成纵向 n1 份、横向 n2 份的格子之后
的第 n 个区域。区域的编号方式是:从左上角开始是 1 号,它的右边
是 2 号,以此类推,当到达最右边之后,就从下一行的左边开始继续
编号。请注意 plt.subplot 中的 n,它比较特别,不是从 0 开始
的,而是从 1 开始的,如果令 n 为 0,就会出现错误。
In
# 代码清单 3-2-(9)
plt.figure(figsize = (10, 3)) # (A) 指定 figure
plt.subplots_adjust(wspace = 0.5, hspace = 0.5) # (B) 指定图形间隔
for i in range(6):
plt.subplot(2, 3, i + 1) # (C) 指定图形的绘制位

plt.title(i + 1)
plt.plot(x, f2(x, i), 'k')
plt.ylim(-20, 20)
plt.grid(True)
plt.show()
Out
代码清单 3-2-(9) 中 (A) 处的 plt.figure(figsize = (w, h))
用于指定整个绘制区域的大小。绘制区域的宽度为 w,高度为 h,当使
用 subplot 并列显示时,可以通过 (B) 处的
plt.subplots_adjust(wspace = w, hspace = h) 调节两个
相邻区域的横向间隔与纵向间隔。w 为横向间隔,h 为纵向间隔,它们
的数值越大,间隔越大。
3.2 绘制三维图形
3.2.1 包含两个变量的函数
如何绘制包含两个变量的函数的图形呢?比如函数:
首先,在代码清单 3-3-(1) 中将上面的函数定义为 f3。然后,计算当
x0 和 x1 取不同的值时,f3 的值是多少。
In
# 代码清单 3-3-(1)
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# 定义函数 f3
def f3(x0, x1):
ans = (2 * x0**2 + x1**2) * np.exp(-(2 * x0**2 + x1**2))
return ans
# 根据 x0 和 x1 计算 f3
xn = 9
x0 = np.linspace(-2, 2, xn) # (A)
x1 = np.linspace(-2, 2, xn) # (B)
y = np.zeros((len(x0), len(x1))) # (C)
for i0 in range(xn):
for i1 in range(xn):
y[i1, i0] = f3(x0[i0], x1[i1])# (D)
(A) 定义了要计算的 x0 的范围。由于 xn = 9,所以运行下面的命令
可知,x0 是由 9 个元素构成的,x1 和 x0 相同((B))。
In
# 代码清单 3-3-(2)
print(x0)
Out
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
通过 (C) 处的代码准备一个用于存放计算结果的二维数组变量 y,然
后在 (D) 处,根据由 x0 和 x1 定义的棋盘上的各个点求 f3,并将结
果保存在 y[i1, i0] 中。请注意这里的元素索引,用于指示 x1 的内
容的 i1 在前,i0 在后。这是为了与后面的显示方向相对应。
下面通过 round 函数,把矩阵 y 四舍五入到小数点后 1 位(为了便于
查看),并输出矩阵。
In
# 代码清单 3-3-(3)
print(np.round(y, 1))
Out
[[ 0. 0. 0. 0. 0.1 0. 0. 0. 0. ]
[ 0. 0. 0.1 0.2 0.2 0.2 0.1 0. 0. ]
[ 0. 0. 0.1 0.3 0.4 0.3 0.1 0. 0. ]
[ 0. 0. 0.2 0.4 0.2 0.4 0.2 0. 0. ]
[ 0. 0. 0.3 0.3 0. 0.3 0.3 0. 0. ]
[ 0. 0. 0.2 0.4 0.2 0.4 0.2 0. 0. ]
[ 0. 0. 0.1 0.3 0.4 0.3 0.1 0. 0. ]
[ 0. 0. 0.1 0.2 0.2 0.2 0.1 0. 0. ]
[ 0. 0. 0. 0. 0.1 0. 0. 0. 0. ]]
仔细看一下矩阵中的数值会发现,矩阵的中心和周围都是 0,看上去就
像一个鼓起来的甜甜圈。但是,只看数值,我们很难想象出函数的形
状。
3.2.2 用颜色表示数值:pcolor
下面我们试着把二维矩阵的元素换成颜色。这里需要使用
plt.pcolor( 二维 ndarray)。
In
# 代码清单 3-3-(4)
plt.figure(figsize = (3.5, 3))
plt.gray() # (A)
plt.pcolor(y) # (B)
plt.colorbar() # (C)
plt.show()
Out
在代码清单 3-3-(4) 中,(A) 用于指定以灰色色调显示图形。除
plt.gray() 以外,还可以通过 plt.jet()、plt.pink() 和
plt.bone() 等指定各种各样的渐变模式。(B) 用于显示矩阵的颜
色,(C) 用于在矩阵旁边显示色阶。
这个函数的图形如图 3-1 所示。
图 3-1 包含两个变量的函数的图形
图 3-1 中 4 张图均为代码执行结果的原图。——编者注
3.2.3 绘制三维图形:surface
接下来,我们介绍一种方法,用于绘制如图 3-1B 所示的三维立体图
形,即 surface(代码清单 3-3-(5))。
In
# 代码清单 3-3-(5)
from mpl_toolkits.mplot3d import Axes3D # (A)
xx0, xx1 = np.meshgrid(x0, x1) # (B)
plt.figure(figsize = (5, 3.5))
ax = plt.subplot(1, 1, 1, projection = '3d') # (C)
1
1
ax.plot_surface(xx0, xx1, y, rstride = 1, cstride = 1, alpha = 0.3,
color = 'blue', edgecolor = 'black') # (D)
ax.set_zticks((0, 0.2)) # (E)
ax.view_init(75, -95) # (F)
plt.show()
Out
要想绘制三维图形,需要导入 mpl_toolkits.mplot3d 的
Axes3D(代码清单 3-3-(5) 中的 (A))。这里在导入时使用的
是“from 库名 import 函数名”的方式,与此前使用的方式略有不
同。这里不再展开说明,大家只需知道,有了这种方法,就能够不使
用“库名.函数”这种方式,而只通过“函数名”即可调用函数。
然后,代码清单 3-3-(5) 中的 (B) 用于根据坐标点 x0、x1 生成
xx0、xx1。
下面试着确认一下变量 x0、x1 的内容(代码清单 3-3-(6))。
In
# 代码清单3-3-(6)
print(x0)
print(x1)
Out
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
通过 np.meshgrid(x0, x1) 生成的 xx0 是如下所示的二维数组。
In
# 代码清单3-3-(7)
print(xx0)
Out
[[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]
[-2. -1.5 -1. -0.5 0. 0.5 1. 1.5 2. ]]
xx1 是如下所示的二维数组。
In
# 代码清单3-3-(8)
print(xx1)
Out
O[[-2. -2. -2. -2. -2. -2. -2. -2. -2. ]
[-1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5]
[-1. -1. -1. -1. -1. -1. -1. -1. -1. ]
[-0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5]
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. ]
[ 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5]
[ 1. 1. 1. 1. 1. 1. 1. 1. 1. ]
[ 1.5 1.5 1.5 1.5 1.5 1.5 1.5 1.5 1.5]
[ 2. 2. 2. 2. 2. 2. 2. 2. 2. ]]
xx0 和 xx1 是与 y 一样大的矩阵,当输入 xx0[i1, i0] 和
xx1[i1, i0] 时,f3 为 y[i1, i0]。
为了在三维坐标系中绘制图形,我们在声明 subplot 时指定了
projection = '3d'(代码清单 3-3-(5) 中的 (C))。然后,把表示
这个图形的 id 的返回值保存在 ax 中。这段代码只在 figure 中指定
了一个 subplot,但实际上也可以像 subplot(n1, n2, n,
project = '3d') 这样指定多个坐标系。
在代码清单 3-3-(5) 中,(D) 中的 ax.plot_surface(xx0, xx1,
y) 用于显示 surface。我们可以把自然数赋给可选项 rstride 与
cstride,来指定纵轴与横轴每隔几个元素绘制一条线。数越少,线
的间隔越短。alpha 是用 0 ~ 1 的实数指定图形透明度的选项,值越
接近 1,越不透明。
如果 轴的刻度采用默认值,那么数值就会重叠在一起。因此,我们
使用 ax.set_zticks((0, 0, 2)) 把 的刻度限定为 0 和 0.2(代
码清单 3-3-(5) 中的 (E))。
(F) 中的 ax.view_init( 变量 1, 变量 2) 用于调节三维图形的
方向,“变量 1”表示纵向旋转角度,当它为 0 时,图形是从正侧面观
察到的图形;当它为 90 时,则是从正上方观察到的图形。“变量 2”表
示横向旋转角度,当它为正数时,图形会按照顺时针方向旋转;当它
为负数时,则会按照逆时针方向旋转。
图 3-1B 是以 9 × 9 的分辨率绘制的函数图形,把分辨率提高到 50 ×
50,即令 rstride = 5、cstride = 5,得到的图形如图 3-1D 所
示。由于只是稍微改动了代码清单 3-3-(1) 和代码清单 3-3-(5),所以这
里不再放改动后的代码。分辨率越高,图形越清晰。这样一来,就可
以更加直观地理解函数的形状。
3.2.4 绘制等高线:contour
要想定量了解函数的高度,一个方便的方法是使用代码清单 3-3-(9) 绘
制等高线(图 3-1C)。
In
# 代码清单3-3-(9)
n = 50
x0 = np.linspace(-2, 2, xn)
x1 = np.linspace(-2, 2, xn)
y = np.zeros((xn, xn))
for i0 in range(xn):
for i1 in range(xn):
y[i1, i0] = f3(x0[i0], x1[i1])
xx0, xx 1= np.meshgrid(x0, x1) # (A)
plt.figure(1, figsize = (4, 4))
cont = plt.contour(xx0, xx1, y, 5, colors = 'black') # (B)
cont.clabel(fmt = '%.2f', fontsize = 8) # (C)
plt.xlabel('$x_0$', fontsiz e= 14)
plt.ylabel('$x_1$', fontsize = 14)
plt.show()
Out
代码清单 3-3-(9) 的前半部分以 50 × 50 的分辨率生成了 xx0、xx1 和
y(到 (A) 为止的代码)。这是因为,如果不把分辨率提升到一定程
度,就无法准确绘制等高线的图形。
(B) 中的 plt.contour(xx0, xx1, y, 5, colors =
'black') 用于绘制等高线。5 用于指定显示的高度共有 5 个级别,
而 colors = 'black' 用于指定等高线的颜色为黑色。
把 plt.contour 的返回值保存在 cont 中,并运行
cont.clabel(fmt = '%.2f', fontsize = 8),可以在各个等
高线上显示高度值((C))。fmt = '%.2f' 用于指定数值格式,
fontsize 选项用于指定字符的大小。
第 4 章 机器学习中的数学
从第 5 章开始,我们就要学习机器学习了,所以本章先总结一下学习
机器学习所需的数学知识。同时,本章还会介绍如何在 Python 中使用
这些知识。熟知数学的读者也可以跳过本章,必要时再回过头来翻一
翻。
4.1 向量
4.1.1 什么是向量
第 5 章会出现向量,向量是由几个数横向或纵向排列而成的。数纵向
排列的向量叫作列向量,如下式 4-1 所示的变量就是列向量:
数横向排列的向量叫作行向量,如下式 4-2 所示的变量就是行向量:
构成向量的一个一个数叫作元素。向量中的元素个数叫作向量的维
度。如上例所示, 为二维列向量, 为四维行向量。如 和 所示,
本书中的向量用小写粗斜体表示。
与向量不同的普通的单个数叫作标量。本书中的标量用小写斜体表
示,如 、。
向量右上角的 T 是转置符号,表示将列向量转换为行向量,或者将行
向量转换为列向量,如下式 4-3 所示:
在本书中,除了从数学上来说必须使用转置符号的情况外,考虑到行
距,有时也会把
写成 等。
4.1.2 用 Python 定义向量
接下来,我们用 Python 定义向量。正如第 3 章中介绍的那样,要想使
用向量,必须先使用 import 导入 NumPy 库(代码清单 4-1-(1))。
In
# 代码清单 4-1-(1)
import numpy as np
然后,如代码清单 4-1-(2) 所示,使用 np.array 定义向量 a。
In
# 代码清单 4-1-(2)
a = np.array([2, 1])
print(a)
Out
[2 1]
运行 type,可以看到 a 的类型为 numpy.ndarray(代码清单 4-1-
(3))。
In
# 代码清单 4-1-(3)
type(a)
Out
numpy.ndarray
4.1.3 列向量的表示方法
接下来介绍如何表示列向量。事实上,一维的 ndarray 类型没有纵横
之分,往往表示为行向量。
不过用特殊形式的二维 ndarray 表示列向量也是可以的。
ndarray 类型可以表示 2 × 2 的二维数组(矩阵),如代码清单 4-1-
(4) 所示。
In
# 代码清单 4-1-(4)
c = np.array([[1, 2], [3, 4]])
print(c)
Out
[[1 2]
[3 4]]
用这个方式定义 2 × 1 的二维数组,就可以用它表示列向量(代码清单
4-1-(5))。
In
# 代码清单 4-1-(5)
d = np.array([[1], [2]])
print(d)
Out
[[1]
[2]]
向量通常定义为一维 ndarray 类型,必要时可以用二维 ndarray 类
型。
4.1.4 转置的表示方法
转置用“变量名.T”表示(代码清单 4-1-(6))。
In
# 代码清单 4-1-(6)
print(d.T)
print(d.T.T)
Out
[[1 2]]
[[1]
[2]]
使用 d.T.T 循环两次转置操作之后,就会变回原来的 d。
请注意,转置操作对于二维 ndarray 类型有效,但对于一维
ndarray 类型是无效的。
4.1.5 加法和减法
接下来,我们思考下面两个向量 和 :
首先进行加法运算。向量的加法运算 是将各个元素相加:
向量的加法运算可以通过图形解释。首先,将向量的元素看作坐标
点,将向量看作一个从坐标原点开始,延伸到元素坐标点的箭头。这
样一来,单纯地将各个元素相加的向量加法运算就可以看作,对以
和 为邻边的平行四边形求对角线(图 4-1)。
图 4-1 向量的加法运算
像这样通过图形理解数学式不仅有趣,而且能让我们深刻理解理论,
并以此为基础创建新的理论。
如代码清单 4-1-(7) 所示,运行 a + b 的加法运算之后,程序会返回
预期的答案,可知 a 和 b 不是 list 类型,而是被当作向量处理的
(对于 list 类型,加法运算的作用是连接)。
In
# 代码清单4-1-(7)
a = np.array([2, 1])
b = np.array([1, 3])
print(a + b)
Out
[3 4]
向量的减法运算与加法运算相同,是对各个元素进行减法运算:
在 Python 中,式 4-6 的计算如代码清单 4-1-(8) 所示。
In
# 代码清单4-1-(8)
a = np.array([2, 1])
b = np.array([1, 3])
print(a - b)
Out
[ 1 -2]
那么,减法运算该怎么借助图形解释呢?
就是 ,可以看作 和 的加法运算。从图形上来说,
的箭头方向与 相反。所以, 是以 和 为邻边的平行
四边形的对角线(图 4-2)。
图 4-2 向量的减法运算
4.1.6 标量积
在标量与向量的乘法运算中,标量的值会与向量的各个元素分别相
乘,比如 :
在 Python 中,式 4-7 的计算如代码清单 4-1-(9) 所示。
In
# 代码清单 4-1-(9)
print(2 * a)
Out
[4 2]
从图形上来说,向量的长度变成了标量倍(图 4-3)。
图 4-3 向量的标量积
4.1.7 内积
向量与向量之间的乘法运算叫作内积。内积是由相同维度的两个向量
进行的运算,通常用“·”表示,这在机器学习涉及的数学中很常见。内
积运算是把对应的元素相乘,然后求和,比如 、
的内积:
在 Python 中,我们使用“变量名 1.dot( 变量名 2)”计算内积 (代
码清单 4-1-(10))。
In
# 代码清单 4-1-(10)
b = np.array([1, 3])
c = np.array([4, 2])
print(b.dot(c))
Out
10
但是,内积表示的究竟是什么呢?如图 4-4 所示,设 在 上的投影
向量为 ,那么 和 的长度相乘即可得到内积的值。
当两个向量的方向大致相同时,内积的值较大。相反,当两个向量近
乎垂直时,内积的值较小;当完全垂直时,内积的值为 0。可以说,内
积与两个向量的相似度相关。
图 4-4 向量的内积
但是,请注意内积与向量自身的大小也相关。即使两个向量方向相
同,只要其中一个向量变成原来的 2 倍,那么内积也会变成原来的 2
倍。
4.1.8 向量的模
向量的模是指向量的长度,将向量夹在两个“||”之间,即可表示向量的
模。二维向量的模可计算为:
三维向量的模则可计算为:
在一般情况下, 维向量的模计算为:
在 Python 中,我们使用 np.linalg.norm() 求向量的模(代码清单
4-1-(11))。
In
# 代码清单 4-1-(11)
a = np.array([1, 3])
print(np.linalg.norm(a))
Out
3.1622776601683795
4.2 求和符号
从 5.1 节开始,求和符号 Σ(西格玛)就会出现。求和符号经常出现
在机器学习的教材中。
比如,下式 4-12 的意思是“将从 1 到 5 的变量 的值全部相加”。
Σ 用于简洁地表示长度较长的加法运算。对上式加以扩展,如式 4-13
所示,它表示“对于 Σ 右边的 ,令变量 的取值从 开始递增 1,
直到 变为 ,然后把所有 相加”(图 4-5)。
图 4-5 求和符号
比如,令 ,则结果如式 4-14 所示。这跟编程中的 for 语句
很像。
4.2.1 带求和符号的数学式的变形
在思考机器学习的问题时,我们常常需要对带求和符号的数学式进行
变形。接下来,思考一下如何变形。最简单的情况是求和符号右侧的
函数 中没有 ,比如 。这时,只需用相加的次数乘以
即可,所以可以去掉求和符号:
当 为“标量 × 的函数”时,可以将标量提取到求和符号的外侧
(左侧):
当求和符号作用于多项式时,可以将求和符号分配给各个项:
之所以可以这样做,是因为无论是多项式相加,还是各项单独相加再
求和,答案都是一样的。
4.1.7 节的向量的内积也可以使用求和符号表示。比如,
和 的内积可以使用“·”表示为
(图 4-6):
图 4-6 矩阵表示法和元素表示法
图 4-6 左侧称为矩阵表示法(向量表示法),右侧称为元素表示法,
而式 4-18 则可以看作在两者之间来回切换的一个式子。
4.2.2 通过内积求和
前面我们说过 Σ 跟编程中的 for 语句很像,根据式 4-18,Σ 也与内
积有关,所以也可以通过内积计算 Σ。例如,从 1 加到 1000 的和为:
在 Python 中,式 4-19 的计算如代码清单 4-2-(1) 所示。与 for 语句
相比,这种方法的运算处理速度更快。
In
# 代码清单 4-2-(1)
import numpy as np
a = np.ones(1000) # [1 1 1 ... 1]
b = np.arange(1,1001) # [1 2 3 ... 1000]
print(a.dot(b))
Out
500500.0
4.3 累乘符号
累乘符号 Π 与 Σ 符号在使用方法上类似。这个符号将会在 6.1 节的分
类问题中出现。Π 用于使 的所有元素相乘(图 4-7):
下式是一个最简单的例子:
下式是累乘符号 Π 作用于多项式的示例:
图 4-7 累乘符号
4.4 导数
在大部分情况下,机器学习的问题可以归结为求函数取最小值(或最
大值)时的输入的问题(最值问题)。因为函数具有在取最小值的地
方斜率为 0 的性质,所以在求解这样的问题时,获取函数的斜率就变
得尤为重要。推导函数斜率的方法就是求导。
5.1.3 节将讲解如何求误差函数的最小值,从这一节开始,导数(偏导
数)就会登场。
4.4.1 多项式的导数
首先,我们以二次函数为例思考一下(图 4-8 左):
图 4-8 函数的导数表示斜率
函数 对 的导数可以有如下多种表示形式:
导数表示函数的斜率(图 4-8 右)。由于当 发生变化时,函数的斜
率也会随之变化,所以函数的斜率也是一个关于 的函数。这个二次
函数是:
在一般情况下,我们可以使用下式简单地求出 形式的函数的导数
(图 4-9)。
图 4-9 幂函数的导数公式
比如,四次函数的导数为:
如果是一次函数,则导数如下式所示。不过,由于一次函数是直线,
所以无论 取值如何,斜率都不会发生变化。
4.4.2 带导数符号的数学式的变形
接下来,我们思考一下带导数符号的数学式该如何变形。跟求和符号
Σ 一样,导数符号 也作用于式子的右侧。
如下面的 所示,当常数出现在 的前面表示相乘时,我们可以把
这个常数提取到导数符号的左侧:
与导数无关的部分(不是 的函数的部分),即使是字符表达式 ,
也可以把它提取到导数符号的左侧。
所谓字符表达式,即由字母、符号构成的表达式。——译者注
如果 中不包含 ,则导数为 0:
那么,下式的导数是什么呢?
这个式子里也不包含 ,所以导数为 0:
当 包含多个带 的项时,比如下面这个式子,它的导数是什么
呢?
1
1
此时,我们可以一项一项地分别进行导数计算:
4.4.3 复合函数的导数
在机器学习中,很多情况下需要求复合函数的导数,比如:
只需简单地将式 4-32 代入式 4-31 中,然后展开,即可计算它的导
数:
4.4.4 复合函数的导数:链式法则
但是,有时式子比较复杂,很难展开。在这种情况下,可以使用链式
法则(图 4-10)。链式法则将从本书 5.1 节开始出现。
链式法则的公式是:
接下来,我们借着式 4-31 和式 4-32 讲解一下链式法则。
首先, 的部分是“ 对 求导”的意思,所以可以套用导数公式,
得到:
后面的 是“ 对 求导”的意思,所以可以得到:
接下来,把式 4-36 和式 4-37 代入式 4-35,就可以得到和式 4-34 的
答案一样的答案了:
链式法则还可以扩展到三重甚至四重嵌套的复合函数中,比如函数:
此时,需要使用如下公式:
图 4-10 链式法则
4.5 偏导数
4.5.1 什么是偏导数
机器学习中不仅会用到导数,还会用到偏导数。偏导数将从本书 5.1
节开始出现。
我们思考一下多变量函数,比如关于 和 的函数:
对于式 4-41,如果只对其中一个变量(比如 )求导,而将其他变量
(这里是 )当作常数,那么求出的就是偏导数(图 4-11)。
图 4-11 偏导数
“ 对 的偏导数”的数学式是:
求偏导数的方法是“只对要求偏导数的变量进行求导”。或许你一听
到“偏导数”就感觉很难,但实际上它的求导过程与普通的导数(常微
分)是一样的。
例如,以前面的式 4-41 中的 来说,就是只关注其中的 ,像
下式这样思考:
套用导数公式之后,得到:
而对于式 4-41 中的 ,则只关注其中的 ,像下式这样解释:
然后,就可以得到:
4.5.2 偏导数的图形
偏导数的图形是什么样的呢?
的函数可以使用第 3 章介绍的三维图形或等高线图形表示。
实际绘制之后会发现,它的图形就像一个两个角被提起来的方巾(图
4-12)。
图 4-12 偏导数的图形意义
为了理解 ,我们可以在与 轴平行的方向上把 切开,然后
观察 的截面(图 4-12 ①)。
截面是一个向下凸出(向上开口)的二次函数,它的曲线斜率可以通
过式 4-44 求得,式子为 (图 4-12 ②)。
当在 的平面上切开时,把 代入式 4-44,即可得到当
时斜率的计算式。
把 代入 之后得到:
这里,使用式 4-44 的结果,可以像下式这样去计算(图 4-12 ②)。这
是一条斜率为 2、截距为 -2 的直线:
平行于 轴的平面有无数个。比如,当在 的平面上切开时,
的截面如图 4-12 ③ 所示,截面的斜率是(图 4-12 ④):
而 是一个平行于 轴的 的截面,这个截面是一条直线。比
如,当在 的平面上切开时,得到的截面如图 4-12 ⑤ 所示,它
的斜率是(图 4-12 ⑥):
又如,当在 的平面上切开时,得到的截面的斜率是(图 4-12
⑦):
总的来说,对 和 的偏导数就是分别求出 方向的斜率和 方
向的斜率。
这两个斜率的组合可以解释为向量。这就是 对 的梯度(梯度向
量,gradient),梯度表示的是斜率最大的方向及其大小。
4.5.3 绘制梯度的图形
下面实际绘制一下梯度的图形。代码清单 4-2-(2) 绘制了 的等高线
(图 4-13 左),并通过箭头绘制了把 的空间分为网格状时各点的梯
度 (图 4-13 右)。
In
# 代码清单 4-2-(2)
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
def f(w0, w1): # (A) 定义 f
return w0**2 + 2 * w0 * w1 + 3
def df_dw0(w0, w1): # (B) f 对 w0 的偏导数
return 2 * w0 + 2 * w1
def df_dw1(w0, w1): # (C) f 对 w1 的偏导数
return 2 * w0 + 0 * w1
w_range = 2
dw = 0.25
w0 = np.arange(-w_range, w_range + dw, dw)
w1 = np.arange(-w_range, w_range + dw, dw)
ww0, ww1 = np.meshgrid(w0, w1) # (D)
ff = np.zeros((len(w0), len(w1)))
dff_dw0 = np.zeros((len(w0), len(w1)))
dff_dw1 = np.zeros((len(w0), len(w1)))
for i0 in range(len(w0)): # (E)
for i1 in range(len(w1)):
ff[i1, i0] = f(w0[i0], w1[i1])
dff_dw0[i1, i0] = df_dw0(w0[i0], w1[i1])
dff_dw1[i1, i0] = df_dw1(w0[i0], w1[i1])
plt.figure(figsize = (9, 4))
plt.subplots_adjust(wspace = 0.3)
plt.subplot(1, 2, 1)
cont = plt.contour(ww0, ww1, ff, 10, color s= 'k') # (F)显示f的等高线
cont.clabel(fmt = '%d', fontsize = 8)
plt.xticks(range(-w_range, w_range + 1, 1))
plt.yticks(range(-w_range, w_range + 1, 1))
plt.xlim(-w_range - 0.5, w_range + 0.5)
plt.ylim(-w_range - 0.5, w_range + 0.5)
plt.xlabel('$w_0$', fontsize = 14)
plt.ylabel('$w_1$', fontsize = 14)
plt.subplot(1, 2, 2)
plt.quiver(ww0, ww1, dff_dw0, dff_dw1) # (G)显示f的梯度向

plt.xlabel('$w_0$', fontsize = 14)
plt.ylabel('$w_1$', fontsize = 14)
plt.xticks(range(-w_range, w_range + 1, 1))
plt.yticks(range(-w_range, w_range + 1, 1))
plt.xlim(-w_range - 0.5, w_range + 0.5)
plt.ylim(-w_range - 0.5, w_range + 0.5)
plt.show()
Out
# 运行结果见图 4-13
代码清单 4-2-(2) 首先在 (A) 处定义了函数 f,然后在 (B) 处定义了
用于返回 方向的偏导数的函数 df_dw0,在 (C) 处定义了用于返回
方向的偏导数的函数 df_dw1。
(D)处的 ww0, ww1 = np.meshgrid(w0, w1) 将网格状分布的 w0
和 w1 存储在了二维数组 ww0 和 ww1 中。(E) 用于根据 ww0 和 ww1
计算 f 和偏导数的值,并将值存储在 ff 和 dff_dw0、dff_dw1 中。
(F)用于将 ff 显示为等高线,(G) 用于将梯度显示为箭头。
用于显示箭头的代码 (G) 是通过 plt.quiver(ww0, ww1,
dff_dw0, dff_dw1) 绘制从坐标点 (ww0, ww1) 到方向
(dff_dw0, dff_dw1) 的箭头的。
图 4-13 梯度向量
通过图 4-13 左侧的 的等高线图形上的数值,我们可以想象到 的地
形是右上方和左下方较高,左上方和右下方较低。图 4-13 右侧是这种
地形的梯度,可以看到箭头朝向的是各个点中斜面较高的方向,而且
斜面越陡(等高线间隔越短),箭头越长。
观察可知,箭头无论从哪个地点开始,都总是朝向图形中地形较高的
部分。相反,箭尾总是朝向地形较低的部分。因此,梯度是用于寻找
函数的最大点或最小点的一个重要概念。在机器学习中,在求误差函
数的最小点时会使用误差函数的梯度(5.1 节)。
4.5.4 多变量的复合函数的偏导数
当嵌套的是多变量函数时,该怎么求导呢?我们会在推导多层神经网
络的学习规则时遇到这个问题(第 7 章)。
比如, 和 都是关于 和 的函数, 是关于函数 和 的函
数。现在我们使用链式法则来表示 对 和 的偏导数(图 4-
14):
图 4-14 偏导数的链式法则
下面先说一下结论,对 求偏导数的式子是:
对 求偏导数的式子是:
比如,当 如下式时,该如何求解 呢?
此时,式 4-54 的构成要素就变成了:
把它们代入式 4-54,即可像下式这样求解,请注意,式 4-57 和式 4-
58 也使用了链式法则:
在实际推导神经网络的学习规则时,使用的往往是像
这样嵌套了至少两个函数的函
数。此时,链式法则是:
4.5.5 交换求和与求导的顺序
在机器学习中,计算时常常需要对一个用求和符号表示的函数求导,
比如(本节将偏导数也称为导数):
单纯地说,应该可以先求和再求导:
但是,实际上即使先求出各项的导数再求和,答案也是一样的:
如果使用求和符号表示上述计算过程,则具体为:
因此,根据式 4-63 和式 4-64,下式成立:
我们可以把它一般化为下式。如图 4-15 所示,可以把导数符号提取到
求和符号的右侧,先进行求导计算。
图 4-15 导数符号和求和符号的互换
我们常常遇到先求导可以令计算更轻松,或者只能求导的情况。因
此,机器学习中经常会用到式 4-66。
比如,我们借用第 5 章中的式 5-8 思考一下:
在求上述函数对 的导数时,要使用式 4-66 将导数符号移至求和符
号的右侧:
然后,求出导数,得到:
这里,在计算 法则的式
子,即 、。
4.6 矩阵
从 5.2 节开始,我们就会用到矩阵。借助矩阵,可以用一个式子表示
大量的联立方程式,特别方便。此外,使用矩阵或向量表示,也会更
有助于我们直观理解方程式。
4.6.1 什么是矩阵
把数横向或纵向排列,得到的是向量;把数像表格一样既横向排列又
纵向排列,得到的就是矩阵。下式表示的是一个 2 × 3 矩阵(图 4-
16):
图 4-16 矩阵
通常,矩阵中横向的内容从上至下读作第 1 行、第 2 行等,纵向的内
容从左至右读作第 1 列、第 2 列等。但在本书中,为了与 Python 中
数组的索引一致,矩阵从 0 行 0 列开始计数,即横向从上至下分别是
第 0 行、第 1 行等,纵向从左至右分别是第 0 列、第 1 列等。
式 4-70 所示的矩阵通常用“2 行 3 列的矩阵”描述。当用一个变量表示
矩阵时,本书用粗斜体的大写字母 表示。矩阵中元素的表示方法
是:
该式表示的是矩阵 中第 行第 列的元素,如:
请注意,元素的序号是从 0 开始的。
在用变量表示矩阵中的元素时,由于元素是标量,所以用斜体的小写
字母表示:
的下标 、 分别是行和列的序号。下标之间的“,”有时会省略,比如
写作 。
向量可以算作一种矩阵。比如,如下列向量可以看作一个 3 行 1 列的
矩阵:
而如下行向量可以看作一个 1 行 2 列的矩阵:
4.6.2 矩阵的加法和减法
在介绍矩阵和联立方程式的关系之前,我们先介绍几个矩阵相关的规
则。首先看一下矩阵的加法运算。下面以 2 × 3 矩阵 和 为例讲
解:
矩阵的加法运算是把对应的元素相加(图 4-17):
图 4-17 矩阵的加法和减法
减法运算与加法运算一样,是把对应的元素相减:
无论是加法运算还是减法运算,两个矩阵的大小(行数和列数)必须
相等。正如第 2 章讲解的那样,要想利用 Python 进行矩阵计算,必须
和进行向量运算时一样,先使用 import 导入 NumPy 库(代码清单
4-3-(1))。
In
# 代码清单 4-3-(1)
import numpy as np
然后,如代码清单 4-3-(2) 所示,使用 np.array 定义矩阵。
In
# 代码清单 4-3-(2)
A = np.array([[1, 2, 3], [4, 5, 6]])
print(A)
Out
[[1 2 3]
[4 5 6]]
在定义向量时,我们只是像 np.array([1, 2, 3]) 这样用了一组
[];而在定义矩阵时,则是先把每行的元素用一组 [] 括住,然后用
[] 把整个内容括起来,使用的是双层结构。下面如代码清单 4-3-(3)
所示定义 B。
In
# 代码清单 4-3-(3)
B = np.array([[7, 8, 9], [10, 11, 12]])
print(B)
Out
[[ 7 8 9]
[10 11 12]]
代码清单 4-3-(4) 可以计算 A + B、A - B,如下所示。
In
# 代码清单 4-3-(4)
print(A + B)
print(A - B)
Out
[[ 8 10 12]
[14 16 18]]
[[-6 -6 -6]
[-6 -6 -6]]
4.6.3 标量积
当矩阵乘以标量值时,结果是所有的元素都乘以标量值(图 4-18):
图 4-18 矩阵的标量积
在 Python 中,矩阵的标量积如代码清单 4-3-(5) 所示。
In
# 代码清单 4-3-(5)
A = np.array([[1, 2, 3], [4, 5, 6]])
print(2 * A)
Out
[[ 2 4 6]
[ 8 10 12]]
4.6.4 矩阵的乘积
矩阵之间的乘积(矩阵积)与加法或减法运算不同,有些复杂,我们
逐步讲解一下。
首先看一看 1×3 矩阵 和 3×1 矩阵 ,这两个矩阵可以分别看作行
向量和列向量,不过这里姑且当作矩阵进行计算(图 4-19):
图 4-19 矩阵和 矩阵的乘积
这两个矩阵的乘积可以计算为:
这就是把 和 当作向量时得到的内积。在 Python 中, 和 的内
积如代码清单 4-3-(6) 所示。
In
# 代码清单4-3-(6)
A = np.array([1, 2, 3])
B = np.array([4, 5, 6])
print(A.dot(B))
Out
32
计算 和 的内积要用 A.dot(B),这是 4.1.7 节介绍过的内容。
A.dot(B) 不仅可以计算向量内积,还可以计算矩阵积。但是这样的
话,会产生一种对行向量 和 计算矩阵积的错觉。
其实,在 Python 中计算矩阵积时,矩阵的行和列会被自动调整为可以
进行计算的形式。此时, 会被看作列向量,这样就可以继续计算内
积了。
顺便一提,如果使用通常的乘法运算符号“*”,则乘法运算会在对应的
元素之间进行,如代码清单 4-3-(7) 所示。
In
# 代码清单4-3-(7)
A = np.array([1, 2, 3])
B = np.array([4, 5, 6])
print(A * B)
Out
[ 4 10 18]
这跟“+”或“-”相同。“/”也一样,是在对应的元素之间进行除法运算(代
码清单 4-3-(8))。
In
# 代码清单4-3-(8)
A = np.array([1, 2, 3])
B = np.array([4, 5, 6])
print(A / B)
Out
[ 0.25 0.4 0.5 ]
接下来,思考一下 为 2×3 矩阵、 为 3×2 矩阵时的情况:
此时,我们把 看作 2 行的行向量,把 看作 2 列的列向量,并以
各自的组合计算内积,然后在对应的位置写上答案(图 4-20)。
图 4-20 矩阵和 矩阵的乘积
具体的计算步骤如下:
与前面一样,在使用 Python 计算时也使用 A.dot(B)(代码清单 4-3-
(9))。
In
# 代码清单 4-3-(9)
A = np.array([[1, 2, 3], [-1, -2, -3]])
B = np.array([[4, -4], [5, -5], [6, -6]])
print(A.dot(B))
Out
[[ 32 -32]
[-32 32]]
在一般情况下,当 为 矩阵、 为 矩阵时, 的大
小为 。当 的列数与 的行数不等时,不能计算矩阵积。
矩阵积的元素 、 计算为(图 4-20 下):
行数和列数相等的矩阵叫作方阵。当 和 均为方阵时,虽然我们可
以计算出 和 的值,但是在一般情况下 是不成立
的,所以在矩阵的乘法运算中,顺序很重要。从这一点来说,矩阵积
与即使改变顺序答案也不变的标量积不同。
4.6.5 单位矩阵
对角元素均为 1、其他元素均为 0 的特殊方阵叫作单位矩阵,用 表
示,如 3×3 的单位矩阵为(图 4-21):
图 4-21 单位矩阵
在 Python 中,np.identity(n) 用于生成 的单位矩阵(代码
清单 4-3-(10))。
In
# 代码清单4-3-(10)
print(np.identity(3))
Out
[[ 1. 0. 0.]
[ 0. 1. 0.]
[ 0. 0. 1.]]
各个元素之后有“.”,这表示矩阵的元素可以是用于表示小数的 float
类型。
单位矩阵与标量“1”类似。任何数乘以 1,结果都还是该数。单位矩阵
也一样,任何矩阵(大小相同的方阵)与单位矩阵相乘,结果都不发
生变化。
比如,3×3 矩阵与单位矩阵相乘:
在 Python 中,上面的计算如代码清单 4-3-(11) 所示。
In
# 代码清单 4-3-(11)
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
I = np.identity(3)
print(A.dot(I))
Out
[[ 1. 2. 3.]
[ 4. 5. 6.]
[ 7. 8. 9.]]
看到这里,你可能会感到迷茫,不知道这里为什么要介绍单位矩阵,
其实这是为了给接下来要介绍的逆矩阵做铺垫。
4.6.6 逆矩阵
如何对矩阵进行除法运算呢?对于标量,除以 3 的运算与乘以 3 的倒
数 1/3 是一样的。一个数的倒数是与其相乘可以得到 1 的数。 的倒数
为 ,也可以表示为 :
与之类似,矩阵也有与其对应的逆矩阵(图 4-22)。
图 4-22 逆矩阵
但是,只有行数和列数相等的方阵才具有逆矩阵。一个方阵 与其逆
矩阵 相乘的结果为单位矩阵 :
在一般情况下,矩阵积的结果与顺序有关,但一个矩阵与其逆矩阵的
积一定是单位矩阵,所以与顺序无关。
比如,当 为 2×2 方阵时,令 ,则 的逆矩阵为:
如果 ,那么 的逆矩阵为:
试着计算 ,可得到单位矩阵:
在 Python 中,np.linalg.inv(A) 用于求 的逆矩阵(代码清单
4-3-(12))。
In
# 代码清单4-3-(12)
A = np.array([[1, 2], [3, 4]])
invA = np.linalg.inv(A)
print(invA)
Out
[[-2. 1. ]
[ 1.5 -0.5]]
如上所示,得到的结果与上面的式 4-89 的结果一样。
这里必须注意,也有一些方阵没有对应的逆矩阵。如果是 2×2 方阵,
那么使得 的矩阵就不存在逆矩阵。因为这样的话,式 4-88
中分数的分母就是 0。
比如矩阵 ,由于 ,所以它没有逆矩阵。
对于 3×3 和 4×4 等较大的矩阵,虽然也可以使用公式求逆矩阵,但是
计算过程很复杂。因此,在机器学习中一般会借用库的力量,使用
np.linalg.inv(A) 求逆矩阵。
4.6.7 转置
关于将列向量转换为行向量、将行向量转换为列向量的转置运算,我
们已经在 4.1 节介绍过了。这个转置运算也可以扩展到矩阵中。
以下式为例说明一下:
把矩阵 的行和列互换,即可得到 的转置 ,结果为(图 4-
23):
图 4-23 转置
使用 Python 实现的代码如代码清单 4-3-(13) 所示。
In
# 代码清单4-3-(13)
A = np.array([[1, 2, 3], [4, 5, 6]])
print(A)
print(A.T)
Out
[[1 2 3]
[4 5 6]]
[[1 4]
[2 5]
[3 6]]
扩展到一般情况,转置之后,矩阵下标的顺序会被替换:
在对 整体进行转置时,如下关系式成立(图 4-23):
转置之后矩阵积的顺序与转置前相反。以 2×2 矩阵为例,如下所示,
可以证明式 4-94 成立:
使用式 4-94 可以简单推导出:
这是把 看作一个整体,先对 与 进行转置,最后对 进行
转置。哪怕是三个矩阵的矩阵积,转置之后,矩阵下标的顺序也会被
替换。是不是像解谜一样?
4.6.8 矩阵和联立方程式
正如 4.6 节开头说过的那样,借助矩阵,我们可以用一个式子表示大
量的联立方程式,特别方便。到此为止的内容都是为使用矩阵做的铺
垫,现在一切终于准备就绪了。接下来,我们尝试用一个矩阵表示两
个联立方程式,并使用矩阵运算求解答案。具体来说,这里以下面的
联立方程式为例(图 4-24):
图 4-24 用矩阵表示法求解联立方程式
对于上面的联立方程式,把式 4-96 代入式 4-97 之后,可以简单地求
出 , 。这里特意通过矩阵的方式求解。首先,将式 4-96 和
式 4-97 变形,得到:
上式可以表示为矩阵:
为什么可以这样表示呢?计算式 4-99 的左边之后,可知下式成立,即
两个列向量相等:
式子左边和右边的向量相等,即矩阵中的对应元素相等,所以式 4-100
与式 4-98 是一个意思。
接下来,要想求出 和 的值,需要把式 4-99 变形为:
因此,首先让式 4-99 的两边乘以 的逆矩阵:
根据逆矩阵的性质可知,左边是一个单位矩阵:
已知单位矩阵乘以 ,结果不变,所以我们可以得到:
通过公式 4-88 计算出如下所示的 的结果:
然后得到:
观察对应的元素可知,我们得到了正确的值,即 , 。
对方程式求解时也需要变形,求出“ ”。从这一点来说,这种方法与
求解方程式的过程是类似的。对于方程式 ,我们会在等式的两边
都乘以 的倒数,将其变形为 的形式。而矩阵是让等式两边都
从左边乘以逆矩阵,将 变形为 的形式。
对于只有两个变量的两个联立方程式,即使用普通方法求解也不算麻
烦,但是当变量和式子增多时,比如有 个式子,这种使用矩阵的方
法就会起到不凡的作用。
4.6.9 矩阵和映射
我们可以通过图形解释向量的加法或减法,同样地,也可以通过图形
解释矩阵运算。矩阵可以看作“把向量转换为另一个向量的规则”。此
外,如果将向量解释为坐标,即空间内的某个点,那么矩阵就可以解
释为“令某点向别的点移动的规则”。
像这样关于从组(向量或点)到组(向量或点)的对应关系的规则叫
作映射,矩阵的映射是一种线性映射。
比如,我们看一下上一节中的矩阵的方程式,即式 4-99 的左边:
将上式展开之后,可得到下式,因此矩阵 可以解释为一个令点
向点 移动的映射:
比如,把向量 代入式 4-104 之后,可得到 ,所以可以说“点
通过这个矩阵移动到点 ”。同样地,也可以说“点 移动
到 ,点 移动到 ”。像这样从各种点移动的情形如图 4-
25 中的左图所示,形状为由内向外的旋涡状。
图 4-25 中的右图为 的逆矩阵的映射,它是由外向内的旋涡
状,与原矩阵的映射的移动方向刚好相反。
图 4-25 矩阵形式的向量的映射
这里,式 4-99 可以解释为这样一个问题:应用矩阵 的映射规
则被移动到 的是哪个点?
答案为:
我们可以这样理解:通过逆矩阵 把移动后的点 恢复到移动
前的位置可知,移动前的位置是点 。
4.7 指数函数和对数函数
第 6 章的分类问题会用到 Sigmoid 函数和 Softmax 函数,这些函数是
通过包含 的指数函数创建的。后面我们需要求解这些函数的导
数。此外,5.4 节的线性基底函数模型中使用的高斯基底函数也是一个
形式的指数函数。
4.7.1 指数
指数是一个基于“乘以某个数多少次”,即乘法的次数的概念,并且不只
是自然数,它还可以扩展到负数和实数,这一点很有意思。指数的定
义与公式如图 4-26 所示。
图 4-26 指数的定义与公式
指数函数的定义是:
如果要强调指数函数中的 ,那么可以称之为“以 为底数的指数函
数”。这里的底数 是一个大于 0 且不等于 1 的数。
观察式 4-105 的图形可知,当 时,函数图形是单调递增的(代码
清单 4-4-(1)、图 4-27);当 时,图形是单调递减的。函数的
输出总为正数。
In
# 代码清单4-4-(1)
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
x = np.linspace(-4, 4, 100)
y = 2**x
y2 = 3**x
y3 = 0.5**x
plt.figure(figsize = (5, 5))
plt.plot(x, y, 'black', linewidth = 3, label = '$y = 2^x$')
plt.plot(x, y2, 'cornflowerblue', linewidth = 3, label = '$y =
3^x$')
plt.plot(x, y3, 'gray', linewidth = 3, label = '$y = 0.5^x$')
plt.ylim(-2, 6)
plt.xlim(-4, 4)
plt.grid(True)
plt.legend(loc = 'lower right')
plt.show()
Out
# 运行结果见图 4-27
图 4-27 指数函数
4.7.2 对数
对数的公式如图 4-28 所示。把指数函数的输入和输出反过来,就可以
得到对数函数。也就是说,对数函数是指数函数的反函数。
图 4-28 对数的定义与公式
我们思考一下下式:
首先,把式 4-106 变形为“ ”的形式,得到:
绘制函数图形(代码清单 4-4-(2))可知,式 4-107 与 的图形相
对于 的直线相互对称(图 4-29)。
In
# 代码清单4-4-(2)
x = np.linspace(-8, 8, 100)
y = 2**x
x2 = np.linspace(0.001, 8, 100) # np.log(0)会导致出错,所以不能包含0
y2 = np.log(x2) / np.log(2) # 通过公式(7)计算以2为底数的log
plt.figure(figsize = (5, 5))
plt.plot(x, y, 'black', linewidth = 3)
plt.plot(x2, y2, 'cornflowerblue', linewidth = 3)
plt.plot(x, x, 'black', linestyle = '--', linewidth = 1)
plt.ylim(-8, 8)
plt.xlim(-8, 8)
plt.grid(True)
plt.show()
Out
# 运行结果见图 4-29
图 4-29 对数函数
使用对数函数可以把过大或过小的数转为便于处理的大小。比如,
可以表示为 的对数,即 ,
可以表示为 。
如果只写作 ,不写出底数,则默认底数为 e。e 是一个为 2.718...
的无理数,又称为自然对数的底数或纳皮尔常数。为什么要特殊对待
这个有零有整的数呢?对此,我们将在 4.7.3 节进行说明。
机器学习中经常出现非常大或非常小的数,在使用程序处理这些数
时,可能会引起溢出(位数溢出)。对于这样的数,可以使用对数防
止溢出。
此外,对数还可以把乘法运算转换为加法运算。对图 4-28(4) 进行扩
展,可以得到:
像这样转换之后,计算过程会更加轻松。因此,对于第 6 章中将会出
现的似然这种以乘法运算形式表示的概率,往往会借助其对数,即似
然对数来进行计算。
我们常常遇到“已知函数 ,求使 最小的 ”的情况。此时,其
对数函数 也在 时取最小值。对数函数是一个单调递增函
数,所以即使最小值改变了,使函数取最小值的那个值也不会改变
(代码清单 4-4-(3)、图 4-30)。这在求最大值时也成立。使 取最
大值的值也会使 的对数函数取最大值。
In
# 代码清单4-4-(3)
x = np.linspace(-4, 4, 100)
y = (x - 1)**2 + 2
logy = np.log(y)
plt.figure(figsize = (4, 4))
plt.plot(x, y, 'black', linewidth = 3)
plt.plot(x, logy, 'cornflowerblue', linewidth = 3)
plt.yticks(range(-4,9,1))
plt.xticks(range(-4,5,1))
plt.ylim(-4, 8)
plt.xlim(-4, 4)
plt.grid(True)
plt.show()
Out
# 运行结果见图 4-30
图 4-30 对数函数取最小值的位置不变
鉴于这个性质,在求 的最小值 时,我们经常通过 求最
小值 。本书第 6 章就会用到这种方法。特别是当 以积的形式表
示时,如式 4-108 所示,通过 log 将其转换为和的形式,就会更容易
求出导数,非常方便。
4.7.3 指数函数的导数
指数函数 对 的导数为(代码清单 4-4-(4)、图 4-31):
这里把 简单地表示为 。函数 的导数是原本的函数式乘
以 的形式。
设 ,那么 log 2 约为 0.69, 的图形会稍微向下缩一些。
In
# 代码清单4-4-(4)
x = np.linspace(-4, 4, 100)
a = 2
y = a**xd
y = np.log(a) * y
plt.figure(figsize = (4, 4))
plt.plot(x, y, 'gray', linestyle = '--', linewidth = 3)
plt.plot(x, dy, color = 'black', linewidth = 3)
plt.ylim(-1, 8)
plt.xlim(-4, 4)
plt.grid(True)
plt.show()
Out
# 运行结果见图 4-31
图 4-31 指数函数的导数
这里有一个特殊情况,即当 时, :
也就是说,当 时,导函数的图形不变(图 4-31 右)。这个性质
在计算导数时特别方便。
因此,以 e 为底数的指数函数的应用很广泛,从 4.7.5 节开始讲解的
Sigmoid 函数、Softmax 函数和高斯函数也常常使用 e。
4.7.4 对数函数的导数
对数函数的导数为反比例函数(代码清单 4-4-(5)、图 4-32):
In
# 代码清单 4-4-(5)
x = np.linspace(0.0001, 4, 100) # 不能定义 0 以下
y = np.log(x)
dy = 1 / x
plt.figure(figsize = (4, 4))
plt.plot(x, y, 'gray', linestyle = '--', linewidth = 3)
plt.plot(x, dy, color = 'black', linewidth = 3)
plt.ylim(-8, 8)
plt.xlim(-1, 4)
plt.grid(True)
plt.show()
Out
# 运行结果见图 4-32
图 4-32 对数函数的导数
6.1 节也会出现 这样的导数,这里设 ,
然后,使用链式法则即可求出导数:
4.7.5 Sigmoid 函数
Sigmoid 函数是一个像平滑的阶梯一样的函数:
也可以写作 ,所以 Sigmoid 函数有时也表示为:
这个函数的图形如图 4-33 所示(代码清单 4-4-(6))。
In
# 代码清单 4-4-(6)
x = np.linspace(-10, 10, 100)
y = 1 / (1 + np.exp(-x))
plt.figure(figsize = (4, 4))
plt.plot(x, y, 'black', linewidth = 3)
plt.ylim(-1, 2)
plt.xlim(-10, 10)
plt.grid(True)
plt.show()
Out
# 运行结果见图 4-33
图 4-33 Sigmoid 函数
Sigmoid 函数会把从负实数到正实数的数转换为 0~1 的数,所以常常
用于表示概率。但这个函数并不是为了使输出范围为 0~1 而刻意创建
的,而是基于一定的条件自然推导出来的。
Sigmoid 函数将在第 6 章的分类问题中登场。此外,在第 7 章的神经
网络中,它也会作为表示神经元的特性的重要函数登场。第 6 章和第
7 章会用到 Sigmoid 函数的导数,所以这里我们先求一下它的导数。
先思考导数公式,为了使其与式 4-113 一致,这里设

的导数为 ,因此可得到:
对式 4-116 略微变形:
这里, 就是 ,所以可以用 改写式子,改写后的式子
非常简洁:
4.7.6 Softmax 函数
已知 , , ,现在我们要保持这些数的大小关系不
动,把它们转换为表示概率的 、、。既然是概率,就必须是 0 ~
1 的数,而且所有数的和必须是 1。
这时就需要用 Softmax 函数。首先,求出各个 的 exp 的和 :
使用式 4-119,可以得到:
下面,我们实际编写代码创建 Softmax 函数,并测试一下(代码清单
4-4-(7))。
In
# 代码清单 4-4-(7)
def softmax(x0, x1, x2):
u = np.exp(x0) + np.exp(x1) + np.exp(x2)
return np.exp(x0) / u, np.exp(x1) / u, np.exp(x2) / u
# test
y = softmax(2, 1, -1)
print(np.round(y,2)) # (A) 显示小数点后两位的概率
print(np.sum(y)) # (B) 显示和
Out
[ 0.71 0.26 0.04]
1.0
前面例子中的 , , 分别被转换为了 ,
, 。可以看到,它们的确是按照原本的大小关系被分
配了 0 ~ 1 的数,而且所有数相加之后的和为 1。
Softmax 函数的图形是什么样的呢?由于输入和输出都是三维的,所
以不能直接绘制图形。因此,这里只固定 ,然后把输入各种
和 之后得到的 和 展示在图形上(代码清单 4-4-(8)、图 4-
34)。
In
# 代码清单4-4-(8)
from mpl_toolkits.mplot3d import Axes3D
xn = 20
x0 = np.linspace(-4, 4, xn)
x1 = np.linspace(-4, 4, xn)
y = np.zeros((xn, xn, 3))
for i0 in range(xn):
for i1 in range(xn):
y[i1, i0, :] = softmax(x0[i0], x1[i1], 1)
xx0, xx1 = np.meshgrid(x0, x1)
plt.figure(figsize = (8, 3))
for i in range(2):
ax = plt.subplot(1, 2, i + 1, projection = '3d')
ax.plot_surface(xx0, xx1, y[:, :, i],
rstride = 1, cstride = 1, alpha = 0.3,
color = 'blue', edgecolor = 'black')
ax.set_xlabel('$x_0$', fontsize = 14)
ax.set_ylabel('$x_1$', fontsize = 14)
ax.view_init(40, -125)
plt.show()
Out
# 运行结果见图 4-34
图 4-34 Softmax 函数
把 固定为 1,再令 和 变化之后, 、 的值会在 0 ~ 1 的范围
内变化(图 4-34 左)。 越大, 越趋近于 1; 越大, 越趋近于
1。图中没有显示 ,不过 是 1 减去 和 得到的差,所以应该可
以想象到 是什么样的。
Softmax 函数不仅可以用在包含三个变量的情况中,也可以用在包含
更多变量的情况中。设变量的数量为 ,Softmax 函数可以表示为:
Softmax 函数的偏导数将在第 7 章出现,这里先求一下。首先,求
对 的偏导数:
这里必须要注意, 也是关于 的函数。因此,需要使用导数公式,
设 , :
这里以 , 思考:
因此,式 4-123 可变形为:
这里使用 ,将式 4-125 表示为:
令人震惊的是,上式的形式竟然跟 Sigmoid 函数的导数公式(式 4-
118)完全相同。
接下来,我们求 对 的偏导数:
这里也设 , ,并使
用:
设 , ,思考如何对 求偏导数:
结果为:
这里使用 , ,可得到:
综合式 4-126 和式 4-129 并加以拓展,得到:
这里, 是一个在 时为 1,在 时为 0 的函数。 也可以表
示为 ,称为克罗内克函数。
4.7.7 Softmax 函数和 Sigmoid 函数
不管怎么说,Softmax 函数和 Sigmoid 函数都是非常相似的。这两个
函数有什么关系呢?下面我们试着思考一下。一个包含两个变量的
Softmax 函数是:
将分子和分母均乘以 并整理,再使用公式 ,可以得
到:
这里代入 ,可以得到 Sigmoid 函数:
也就是说,把包含两个变量的 Softmax 函数的输入 和 ,用它们的
差 表示,就可以得到 Sigmoid 函数。也可以说,把
Sigmoid 函数扩展到多个变量之后得到的就是 Softmax 函数。
4.7.8 高斯函数
高斯函数可表示为:
如图 4-35 左图中的黑线所示,高斯函数的图形以 为中心,呈吊
钟形。高斯函数将在第 5 章中作为曲线的近似基底函数登场。
图 4-35 高斯函数
用 表示这个函数图形的中心(均值),用 表示分布的幅度(标准
差),用 表示高度,则高斯函数为(图 4-35 左图中的灰线):
下面尝试绘制它的图形(代码清单 4-4-(9)、图 4-35 左)。
In
# 代码清单 4-4-(9)
def gauss(mu, sigma, a):
return a * np.exp(-(x - mu)**2 /(2 * sigma**2))
x = np.linspace(-4, 4, 100)
plt.figure(figsize = (4, 4))
plt.plot(x, gauss(0, 1, 1), 'black', linewidth = 3)
plt.plot(x, gauss(2, 2, 0.5), 'gray', linewidth = 3)
plt.ylim(-.5, 1.5)
plt.xlim(-4, 4)
plt.grid(True)
plt.show()
Out
# 运行结果见图 4-35
高斯函数有时会用于表示概率分布,在这种情况下,要想使得对 求
积分的值为 1,就需要令式 4-135 中的 为:
4.7.9 二维高斯函数
高斯函数可以扩展到二维。二维高斯函数将在第 9 章的混合高斯模型
中出现。
设输入是二维向量 ,则高斯函数的基本形式为:
二维高斯函数的图形如图 4-36 所示,形似一个以原点为中心的同心圆
状的吊钟。
图 4-36 一个简单的二维高斯函数
在此基础上,为了能够移动其中心,或使其变细长,这里添加几个参
数,得到:
如此一来,exp 中就会有向量或矩阵,或许这会让你感到惊慌失措,
但是别担心,接下来我们逐个介绍。
首先,参数 和 表示的是函数的形状。 是均值向量(中心向
量),表示函数分布的中心:
被称为协方差矩阵,是一个如下所示的 2×2 矩阵:
我们可以给矩阵中的元素 和 赋一个正值,分别用于调整 方向
和 方向的函数分布的幅度。对于 ,则赋一个正的或负的实数,
用于调整函数分布方向上的斜率。如果是正数,那么函数图形呈向右
上方倾斜的椭圆状;如果是负数,则呈向左上方倾斜的椭圆状(设
为横轴, 为纵轴时的情况)。
虽然我们往式 4-138 的 exp 中引入的是向量和矩阵,但变形之后却会
变为标量。简单起见,我们设 ,然后试着计算
,可知 exp 中的值是一个由 和 构成的二次表
达式(二次型):
也可以看作一个控制函数大小的参数,当用在二维高斯函数中表示概
率分布时,我们将其设为:
在进行如上所示的变形后,输入空间的积分值为 1,函数可以表示概率
分布。
式 4-142 中的 是一个被称为“ 的矩阵式”的量,当矩阵大小为 2×2
时, 的值为:
因此, 可以表示为:
下面试着通过 Python 程序绘制一下函数图形。首先,如代码清单 4-5-
(1) 所示定义高斯函数。
In
# 代码清单4-5-(1)
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
%matplotlib inline
# 高斯函数 -----------------------------
def gauss(x, mu, sigma):
N, D = x.shape
c1 = 1 / (2 * np.pi)**(D / 2)
c2 = 1 / (np.linalg.det(sigma)**(1 / 2))
inv_sigma = np.linalg.inv(sigma)
c3 = x - mu
c4 = np.dot(c3, inv_sigma)
c5 = np.zeros(N)
for d in range(D):
c5 = c5 + c4[:, d] * c3[:, d]
p = c1 * c2 * np.exp(-c5 / 2)
return p
输入数据 x 是 矩阵,mu 是模为 2 的向量,sigma 是 2 × 2 矩
阵。下面代入适当的数值测试一下 gauss(s, mu, sigma)(代码清
单 4-5-(2))。
In
# 代码清单4-5-(2)
x = np.array([[1, 2], [2, 1], [3, 4]])
mu = np.array([1, 2])
sigma = np.array([[1, 0], [0, 1]])
print(gauss(x, mu, sigma))
Out
[ 0.15915494 0.05854983 0.00291502]
由上面的结果可知,函数返回了与代入的三个数值相应的返回值。绘
制该函数的等高线图形和三维图形的代码如代码清单 4-5-(3) 所示。
In
# 代码清单4-5-(3)
X_range0 = [-3, 3]
X_range1 = [-3, 3]
# 显示等高线 --------------------------------
def show_contour_gauss(mu, sig):
xn = 40 # 等高线的分辨率
x0 = np.linspace(X_range0[0], X_range0[1], xn)
x1 = np.linspace(X_range1[0], X_range1[1], xn)
xx0, xx1 = np.meshgrid(x0, x1) x = np.c_[np.reshape(xx0, xn *
xn, 1), np.reshape(xx1, xn * xn, 1)]
f = gauss(x, mu, sig)
f = f.reshape(xn, xn)
f = f.T
cont = plt.contour(xx0, xx1, f, 15, colors = 'k')
plt.grid(True)
# 三维图形 ----------------------------------
def show3d_gauss(ax, mu, sig):
xn = 40 # 等高线的分辨率
x0 = np.linspace(X_range0[0], X_range0[1], xn)
x1 = np.linspace(X_range1[0], X_range1[1], xn)
xx0, xx1 = np.meshgrid(x0, x1)
x = np.c_[np.reshape(xx0, xn * xn, 1), np.reshape(xx1, xn * xn,
1)] f = gauss(x, mu, sig)
f = f.reshape(xn, xn)
f = f.T
ax.plot_surface(xx0, xx1, f,
rstride = 2, cstride = 2, alpha = 0.3,
color = 'blue', edgecolor = 'black')
# 主处理 -----------------------------------
mu = np.array([1, 0.5]) # (A)
sigma = np.array([[2, 1], [1, 1]]) # (B)
Fig = plt.figure(1, figsize = (7, 3))
Fig.add_subplot(1, 2, 1)
show_contour_gauss(mu, sigma)
plt.xlim(X_range0)
plt.ylim(X_range1)
plt.xlabel('$x_0$', fontsize = 14)
plt.ylabel('$x_1$', fontsize = 14)
Ax = Fig.add_subplot(1, 2, 2, projection = '3d')
show3d_gauss(Ax, mu, sigma)
Ax.set_zticks([0.05, 0.10])
Ax.set_xlabel('$x_0$', fontsize = 14)
Ax.set_ylabel('$x_1$', fontsize = 14)
Ax.view_init(40, -100)
plt.show()
Out
# 运行结果见图 4-37
运行程序之后,可以得到如图 4-37 所示的图形。图形分布的中心为程
序设定的中心,位于 (1, 0.5)((A))。此外,由于 ,所以图形
分布呈向右上倾斜的形状((B))。
图 4-37 一般的二维高斯函数
第 5 章 有监督学习:回归
终于要开始学习机器学习的内容了。本章将运用第 4 章介绍的数学知
识,具体地讲解机器学习中最重要的有监督学习。有监督学习可以进
一步细分为回归和分类。回归是将输入转换为连续数值的问题,而分
类是将输入转换为没有顺序的类别(标签)的问题。本章讲解回归问
题,第 6 章讲解分类问题。
5.1 一维输入的直线模型
这里我们思考一组年龄 和身高 的数据。假设我们拥有 16 个人的数
据。汇总后的数据以列向量的形式表示为:
表示人数, 。通常人们使用从 1 到 对数据进行编号,但本
书遵循 Python 数组变量的索引习惯,使用从 0 到 对 个数据
进行编号。
这里把 称为输入变量,把 称为目标变量。 表示每个人的数据的
索引。同时,把汇总了所有数据的 称为输入数据,把 称为目标数
据。我们的目标是创建一个函数,用于根据数据库中不存在的人的年
龄 ,预测出这个人的身高 。
首先编写以下代码清单 5-1-(1),创建年龄和身高的人工数据(图 5-
1)。至于具体如何生成数据,将会在本章的最后揭晓,所以现在大家
先不用花时间研究这个问题。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

___Y1

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

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

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

打赏作者

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

抵扣说明:

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

余额充值