函数、向量与矩阵
在导读里,我们让一条直线预测打印耗时:
给它 24 页,它就回答 92 秒。到这里,只有一个输入、一个输出,看起来还很轻巧。
现在给问题加一点变化:一份文档里既有黑白页,也有彩色页,怎么办?如果同时来了三份文档,要逐份写三遍公式吗?再多一台速度不同的打印机,又该怎样比较?
函数、向量和矩阵,正好能帮我们把这些事情说清楚。你可以把这一章看作一次整理:先把计算写成函数,再把相关的数字装进向量,最后把一批计算排进矩阵。每次引入一个新名字,都让它做一点实际的事。
本章需要的数学不超过一次函数、坐标和勾股定理。点积的角度解释会用到余弦;如果暂时忘了,也可以先看例子,稍后再回来。
1. 函数:给计算规则起个名字
同一条规则,可以反复使用
把导读里的模型记作:
是函数的名字, 是输入。 的意思是“把 24 交给函数 计算”,因此 。
函数规定了输入和输出的对应关系:每一个允许的输入,都有唯一的输出。这里允许的输入是文档页数,实际使用时应是非负整数;纸上画直线时,我们也可以把它扩展到实数,观察整条曲线。
这里的“唯一”说的是给定输入后,输出已经确定,并不要求不同输入一定得到不同输出。例如,一个总是返回 72.5 的基线,也是函数。
在 Python 里,可以这样写:
def predict(pages):
return 3 * pages + 20
数学函数与这段程序讲的是同一件事。只是一般的程序函数还可能读写文件、修改状态;此刻我们专注于这种给定输入、计算输出的简单形式。
函数也不一定画成直线。例如,正方形的边长为 时,面积函数是 :边长从 2 变成 4,面积从 4 变成 16。输入翻倍,输出却变成了四倍。我们先从直线入手,是因为容易算、容易画;“输入经过一条规则得到输出”这个想法,适用于更丰富的关系。
输入在变,还是规则在变
如果换一台更慢的打印机,可能需要每页 4 秒;如果启动更慢,固定开销可能变成 40 秒。把两个数留成可调的参数,就得到:
下标 提醒我们:这里其实有一整组函数。固定 、,才选中了其中一条。
- 改变 :还是同一个模型,只是换了一份文档。
- 改变 或 :计算规则变了,也就是换了模型参数。
看看这几条线:
图 1: 决定页数增加时耗时涨得多快; 决定直线从多高的位置开始。图中不同线型对应不同参数,都是用于说明关系的假设。
导读中的训练,就是借助数据挑选参数。这一章先把参数设好,集中研究怎样组织计算,暂时不再训练新模型。
一个函数的输出,也能交给另一个函数
假如界面希望显示分钟,而模型输出秒,可以再写一个换算函数:
是秒数。先算 ,再把结果交给 :
这叫函数复合,也就是把两次计算接起来。程序里对应 g(f(24))。
顺序要看清楚:内层先算,外层后算。这里先预测秒数、再换算成分钟才符合含义。以后看到一层接一层的神经网络,也可以先从“上一层的输出,交给下一层”这个角度理解。
2. 向量:一份文档,不止一个数字
现在允许文档包含黑白页和彩色页。我们把每一页归到其中一类,不重复计数。一份有 10 页黑白、2 页彩色的文档,可以记成:
这就是一个向量:按约定顺序排好的一组数。粗体 表示整组数, 和 表示其中的第一个、第二个分量。
顺序很重要。这里约定第一项是黑白页数,第二项是彩色页数。把它们交换,就变成了另一份文档。数字本身不会告诉程序自己代表什么,解释数字的约定也必须一起保留。
一个数,例如固定耗时 20,叫标量。上面的向量有两个分量,是一个二维向量。遇到 ,可以读成“ 是由两个实数组成的向量”;符号 表示实数集合。虽然页数只取非负整数,我们仍可以把这样的记录放进实数向量中讨论。
同一组数,也可以画成箭头
暂时放下页数,看看平面中的向量:
它可以表示“向右走 3 步,再向上走 2 步”。从原点出发,箭头就指向坐标 。箭头平移到别的位置,只要方向和长度不变,表示的仍是同一个位移向量。
向量既可以是一份特征记录,也可以是一段位移。后者很适合用来理解运算,但不要因此认为所有特征都必须有一个真实的空间方向。等到向量有几百个分量时,画不出来也不影响逐项计算。
相加与缩放,都从每个分量开始
再取一个向量 。这里的上标 表示转置,把横着写的一行转成竖着的一列;这样可以在一句话中紧凑地写出列向量。
把两个向量相加,就是对应位置相加:
从走路的角度看,先走 ,再从它的终点走 ,总位移就是 。
图 2:向量相加可以首尾相接。虚线箭头与实线橙色箭头代表同一个 ,只是起点不同。
乘上一个标量,则让每个分量都乘它:
乘 2,箭头沿原方向变成两倍长;乘 ,箭头掉头,长度不变。两个向量相减,也可以理解成加上反方向的向量。
在打印任务中,两份文档的页数向量相加,可以算出合计的黑白页数和彩色页数。但这不表示把两份任务合并后,总耗时一定等于原来两份耗时之和:固定开销是付一次还是两次,还要看实际怎样提交任务。
3. 点积:把几项贡献合成一个数
从页数到耗时
先假设每页黑白需要 3 秒,每页彩色需要 5 秒,每份文档还有 20 秒固定开销。这些是本章为便于计算设定的参数,并非从彩色打印数据训练出来的结果,也不代表真实设备的速度。
对于 10 页黑白、2 页彩色的文档:
预计耗时为 60 秒。先不看最后的 20,前面的计算就是“对应相乘,再全部相加”。这个操作叫点积。
把两个每页耗时装进权重向量 ,就可以写成:
“权重”在这里描述每个特征对结果的影响:黑白页数每增加 1,预测增加 3 秒;彩色页数每增加 1,预测增加 5 秒。最后加上的固定项通常叫偏置,记作 。于是模型变成:
看一眼单位,也能帮我们检查这个式子:页数乘以每页耗时,得到的是秒。黑白页贡献 30 秒,彩色页贡献 10 秒,两项可以相加;再加上 20 秒固定开销,才是完整的预测耗时。这里的权重不只是两个数字,它们还带着“秒/页”的含义。
两个参数乘两个特征,只是开头。如果有 个特征,规则仍然一样:
是求和符号,意思是让 从 1 走到 ,把每一项 加起来。向量必须有同样多的分量,并且对应位置的含义要对得上。
点积还有一个几何解释
回到刚才的箭头 和 :
用勾股定理, 的长度为:
两侧的双竖线表示这里使用的欧几里得长度,也叫 L2 范数。你可以先把它当作勾股定理在向量上的写法。
点积为什么会和角度扯上关系?先把两根箭头的起点放在一起,从 的终点向 所在的直线作垂线,垂足记作 。从原点到 的这段路,就是 沿 方向留下的“影子”,数学上叫投影。
图 3:把橙色箭头投到蓝色箭头所在的直线上。虚线与蓝色方向垂直,绿色线段是投影;它比橙色箭头短。
图中的夹角记作 。还记得直角三角形里的“邻边除以斜边”吗?橙色箭头是斜边,所以投影长度等于 。再乘上蓝色箭头的长度,就得到点积。在通常的直角坐标系中,两个非零向量满足:
这里的投影要带上方向:如果夹角超过 ,垂足就落在 的反方向上,投影的有向长度为负。向量越朝着同一个方向,余弦越接近 1;垂直时余弦为 0;指向相反方向时余弦为 。因此,点积同时受到长度和方向影响。
回到图中的两个向量,点积是 7,蓝色箭头的长度是 ,所以绿色投影的长度为:
图形与数字说的是同一件事: 长约 2.24,沿着 的方向看,只留下了约 1.94。
选读:对应相乘再相加,为什么会得到“长度乘余弦”?
如果还记得高中学过的余弦定理,可以把这两种写法接起来。把两个非零平面向量的起点放在原点,记终点间的距离为 ,夹角为 。
先用坐标算距离,并把平方展开:
再把两根箭头和连接终点的线段看成一个三角形。根据余弦定理,同一个距离也满足:
两种算法求的是同一个 。消去相同的长度平方项,就得到前面的点积公式。两根箭头同向或反向时,三角形退化成线段,代入 或 ,等式仍成立。
所以,坐标里的“对应相乘再相加”和图形里的“长度与方向”,其实在描述同一个量。暂时忘了余弦定理也没关系:先会用具体向量核对,日后再回来补上这一步。
取几个容易核对的例子:
| 与 比较的向量 | 点积 | 方向关系 |
|---|---|---|
| 13 | 同向 | |
| 0 | 垂直 | |
| 反向 |
如果只想比较方向,就可以除掉两个长度,得到余弦相似度:
零向量没有方向,也不能放进这个除法。对于其他向量,点积大也未必意味着方向更接近:把一个向量放大十倍,就会让点积也放大十倍,方向却完全没变。
以后讨论文本向量和检索时,我们会再遇到这些量。眼下先记住,它们是计算工具;特征的单位、尺度和表示方式,会影响“远近”与“相似”的含义。把米换成厘米,数字扩大百倍,实际物体却并没有走远。
4. 矩阵:把一批任务排在一起
一行一份文档,一列一种特征
假设现在有三份文档:
| 文档 | 黑白页数 | 彩色页数 |
|---|---|---|
| 第一份 | 10 | 2 |
| 第二份 | 4 | 6 |
| 第三份 | 8 | 0 |
把表格中的数字保留下来,就得到矩阵:
矩阵就是按行、列排列的数。 这个矩阵有 3 行、2 列,形状记作 。在本章的数据约定中,行数告诉我们有多少条样本,列数告诉我们每条样本有多少个特征。
记住“先行、后列”,再看 就很自然了:它是第 2 行、第 1 列,也就是第二份文档的黑白页数。这里数学下标从 1 开始,Python 列表下标从 0 开始,所以代码对应的是 X[1][0]。
我们此前把单份文档写成列向量 ,这里却把它放在了矩阵的一行。两者不矛盾:这一行写的是 。转置只交换行列位置,不改变每个数代表的内容。
例如,整个矩阵转置后:
形状从 变成 。现在每一列是一份文档,每一行是一种特征。无论选哪种排法,后面的计算都要遵守同一套约定。
矩阵乘向量,就是每一行做一次点积
给三份文档都用同一组权重 。逐行计算,得到:
这就是矩阵与向量相乘。每一行提供一份文档的特征,与权重做一次点积,于是每一行得到一个数。
最后,每个结果都要加一次 20 秒固定开销:
三份文档分别预测为 60、62、44 秒。这里算的是每份文档单独作为一次任务时的耗时,没有把排队等待或合并打印算进去。
还记得前面说过,“页数可以相加,耗时却未必直接相加”吗?现在就能算清楚了。把前两份文档合成一个任务,共有 14 页黑白、8 页彩色。沿用同一条预测规则,只付一次固定开销:
分开提交时,预测耗时之和是 秒;合成一个任务后,预测为 102 秒。少掉的 20 秒,恰好是省下来的一次固定开销。页数向量相加没有问题,但算耗时之前,还得说清楚我们在处理几次任务。
如果希望写成一句紧凑的公式,可以用 表示全为 1 的向量:
就是 。这样相加的两个对象都是三项列向量,每一项的固定开销都清清楚楚。
再检查一下形状: 是 , 作为列向量是 ,输出是 。中间的两个 2 对应同样的两种特征;它们必须匹配,逐项相乘才有意义。
5. 矩阵乘矩阵:一次比较两台打印机
现在多了一台打印机。第一台仍然使用权重 ,第二台假设使用 。把两组权重并排放好:
这里,每列是一台打印机的权重。第一行对应黑白页,第二行对应彩色页。别因为它恰好有两行两列,就忽略行、列各自的含义。
我们要把三份文档分别交给两套规则计算。结果的第 1 行、第 2 列,应该是“第一份文档在第二台打印机上的加权耗时”:
用同样的办法填完其他位置:
结果中的每一项,都是左边的一行与右边的一列做点积。 这就是矩阵乘法的计算规则。
不妨用手指沿着下面的图走一遍:先选中第一份文档,再找到第二台打印机,黑白页配黑白页的耗时,彩色页配彩色页的耗时。算出的 28,落在结果表中这份文档与这台打印机相交的位置。
图 4:选一行、配一列,就能填好结果中的一格。底部其余数字用同样的方法算出,暂时都没有加固定开销。
为什么非得“一行配一列”?因为我们把一份文档的特征横着排,把一台打印机的权重竖着排。两边都按“黑白、彩色”的顺序,对应位置才是一对可以相乘的数。矩阵乘法把刚才逐份、逐台的点积统一写了出来。
若左边有 份样本、每份 个特征,右边有 组权重,那么:
这里 是乘积的名字。共同的 是每次点积要计算的项数,结果则保留了 份样本和 个输出。用下标写,就是:
选左边的一行, 选右边的一列, 沿着共同的特征逐项走过去。看不顺眼时,先回到刚才的 ,它就是这个式子的一个具体位置。
还差固定开销。假设第一台为 20 秒,第二台为 30 秒,就给每一行的第一列加 20、第二列加 30,得到:
于是,这个简单模型认为第一份文档在第二台上更快,第二份一样快,第三份在第一台上更快。所有数字都来自设定好的规则,目前没有真实数据来说明两台设备是否真的如此。
在一些数值计算库里,把同一组偏置自动加到每一行的做法会用到广播。它是一套明确的形状规则,并不意味着任意两个不同形状的数组都可以相加。本章代码会把逐行加偏置的过程直接写出来。
别把两种乘法混在一起
向量 和 对应位置相乘,得到的是 ;做点积,则还要加起来,得到 7。前一种叫逐元素乘法,结果中保留了每个位置;后一种把它们合成了一个数。
矩阵也一样:逐元素乘法通常要求同样的形状,而矩阵乘法要求左边的列数等于右边的行数。具体库可能额外支持广播,使用时仍需查清楚约定。
还有一个容易踩的坑:矩阵乘法通常不能交换顺序。本例 能算, 却连形状都对不上: 右乘 ,中间的 2 和 3 不相等。即便两种顺序都能算,结果也未必相同。下一节会看到,顺序其实对应“先做哪一步”。
6. 矩阵还能改变一张方格纸
刚才把矩阵看成表格,是为了管理数据。现在换个角度:把一个矩阵看成作用在向量上的函数。
取:
给它一个平面向量 ,它会输出:
第二个坐标保持不变;第一个坐标先加倍,再加上原来的第二个坐标。例如:
如果把方格纸上的每个点都这样变换,原来的正方形会变成平行四边形:
图 5:蓝色和橙色箭头分别表示水平、竖直方向的单位向量。矩阵 把它们送到 和 ;绿色点从 移到 。
这个变换有一个很方便的观察方法。向量 可以理解成“ 份水平单位向量,加上 份竖直单位向量”。变换之后:
右边那两个向量,恰好就是 的两列。矩阵的每一列,告诉我们对应的单位向量被送到了哪里。 这就是理解矩阵几何意义的一条捷径。
矩阵乘向量给出的是线性变换。它保留向量相加和标量缩放的关系:
是一个标量。因此,“先把两段位移加起来,再变换”和“分别变换,再相加”会得到同一结果。线性变换也一定把零向量送回零向量。
如果在变换后再加一个固定向量,让整张图平移,就得到仿射变换。严格地说,带有非零固定项的 也是仿射函数;机器学习里仍习惯把带偏置的这类预测方法称为线性模型。两个名字出现在不同语境中,不必因此怀疑前面的直线。
再想想函数复合:先做矩阵 表示的变换,再做矩阵 表示的变换,可以写成 。右边的 先作用,这和 g(f(x)) 从内向外计算是同样的顺序。练习里,我们会亲手比较换序前后的结果。
7. 从形状看懂张量
一份文档有两个数字,三份文档排成两条轴的表格。如果数据是一批彩色图片,情况又会怎样?
一张图片有高度、宽度,每个位置还有红、绿、蓝三个通道。再把多张图片放在一起,就需要更多条轴来定位一个数字。
在机器学习代码里,我们通常用张量来称呼这种规则排列的多维数组。本章先采用这个计算上的理解;不需要现在就掌握更一般的数学定义。
| 数据 | 一种形状写法 | 每条轴代表什么 |
|---|---|---|
| 一个标量 | () | 没有数组轴 |
| 两个分量的一维数组 | (2,) | 特征 |
| 三份文档,每份两个特征 | (3, 2) | 样本、特征 |
| 八张 32 × 32 的 RGB 图片 | (8, 32, 32, 3) | 样本、高度、宽度、颜色通道 |
括号中的数字依次告诉我们每条轴有多长;(2,) 中的逗号表示这个形状只有一条轴。
这里有两个“维”容易混淆:一个有 100 个分量的向量,可以叫 100 维向量;保存它的一维数组,却只有一条轴。前者说有多少个坐标分量,后者说要用几个索引才能取到一个数。
还有,纸上的列向量和代码里的一维数组也要区分。长度为 2 的一维数组形状是 (2,),显式的列矩阵形状是 (2, 1),行矩阵则是 (1, 2)。它们都存两个数,但在数组运算中并不总能互换。后面的标准库代码用一维列表保存向量,行列含义由我们调用的运算决定。
不同程序也可能把图片通道放在前面,使用 (8, 3, 32, 32)。所以,看到一个形状时,不只要数数字,还要问:这一条轴究竟代表什么?
以后遇到形状错误,可以按三步查:先解释每条轴的含义,再核对运算要求,最后检查结果应该保留哪些轴。不要仅仅为了让程序跑通,就随意交换行列或重排元素。
8. 用几十行 Python 把它们串起来
运行方式
下面的脚本只使用 Python 标准库,Python 3.9 或更高版本即可运行。
如果已经下载本书仓库,在根目录执行:
python3 src/examples/vectors_matrices.py
也可以单独下载脚本,在保存它的目录执行:
python3 vectors_matrices.py
部分电脑上的命令是 python 或 py,换成自己的可用命令即可。
先看懂三个小步骤
dot 做点积,matvec 让矩阵的每一行做一次点积,matmul 再把“每行配每列”的计算展开。
matmul 里的三个循环和公式一一对应:i 选择结果的行,j 选择列,k 沿共同的特征维度,把乘积累加起来。这里的 shape 是我们自己写的函数,用来检查二维列表是否为矩形,并返回行数、列数。
zip 会把两个列表的元素逐项配对,但遇到较短的列表就停止,所以 dot 会先检查分量个数,避免漏掉分量。这里比较的是 len 返回的元素数量,不是几何上的向量长度。列表的 + 在 Python 中表示拼接,* 与整数一起使用表示重复;它们不会自动替我们完成这里的向量加法和矩阵乘法。
"""第一章示例:Python 3.9+,只使用标准库。参数均为教学设定。"""
from math import sqrt
JOBS = [[10, 2], [4, 6], [8, 0]] # 每行依次是黑白页数、彩色页数
WEIGHTS = [3, 5] # 两类页面分别需要的秒数
BIAS = 20 # 每份文档的固定开销
MODEL_WEIGHTS = [[3, 2], [5, 4]] # 每列对应一台打印机
MODEL_BIASES = [20, 30]
U = [3, 2]
V = [1, 2]
TRANSFORM = [[2, 1], [0, 1]]
def linear(pages, a=3, b=20):
return a * pages + b
def dot(u, v):
if len(u) != len(v):
raise ValueError("点积要求两个向量的分量数相同")
return sum(left * right for left, right in zip(u, v))
def shape(matrix):
if not matrix or not matrix[0]:
raise ValueError("这个示例只处理非空矩阵")
columns = len(matrix[0])
if any(len(row) != columns for row in matrix):
raise ValueError("矩阵的每一行必须一样长")
return len(matrix), columns
def matvec(matrix, vector):
_, columns = shape(matrix)
if columns != len(vector):
raise ValueError("矩阵的列数必须等于向量的分量数")
return [dot(row, vector) for row in matrix]
def matmul(left, right):
rows, inner = shape(left)
right_rows, columns = shape(right)
if inner != right_rows:
raise ValueError("左矩阵的列数必须等于右矩阵的行数")
result = []
for i in range(rows):
row = []
for j in range(columns):
value = 0
for k in range(inner):
value += left[i][k] * right[k][j]
row.append(value)
result.append(row)
return result
def main():
seconds = linear(24)
print(f"24 页:{seconds} 秒,约 {seconds / 60:.2f} 分钟")
print("u + v =", [a + b for a, b in zip(U, V)])
print("2u =", [2 * value for value in U])
print("u · v =", dot(U, V))
u_length = sqrt(dot(U, U))
v_length = sqrt(dot(V, V))
print(f"u 的长度:{u_length:.3f}")
print(f"v 沿 u 方向的投影长度:{dot(U, V) / u_length:.3f}")
print(f"u 与 v 的余弦相似度:{dot(U, V) / (u_length * v_length):.3f}")
print("X 的形状:", shape(JOBS))
weighted = matvec(JOBS, WEIGHTS)
print("Xw =", weighted)
print("一台打印机的预测(秒):", [value + BIAS for value in weighted])
raw = matmul(JOBS, MODEL_WEIGHTS)
predicted = [[value + MODEL_BIASES[j] for j, value in enumerate(row)]
for row in raw]
print("XW =", raw)
print("两台打印机的预测(秒):", predicted)
print("A 作用于 [1, 2]:", matvec(TRANSFORM, [1, 2]))
if __name__ == "__main__":
main()
对照手算结果
运行后应得到:
24 页:92 秒,约 1.53 分钟
u + v = [4, 4]
2u = [6, 4]
u · v = 7
u 的长度:3.606
v 沿 u 方向的投影长度:1.941
u 与 v 的余弦相似度:0.868
X 的形状: (3, 2)
Xw = [40, 42, 24]
一台打印机的预测(秒): [60, 62, 44]
XW = [[40, 28], [42, 32], [24, 16]]
两台打印机的预测(秒): [[60, 58], [62, 62], [44, 46]]
A 作用于 [1, 2]: [4, 2]
这里没有随机过程,也没有训练步骤。代码是在执行我们设定的函数和矩阵运算。u 的长度只显示了三位小数; 本身并不恰好等于 3.606。
第一次跑通后,试着把第一份文档的彩色页数从 2 改为 3。先不运行,猜猜两台打印机的预测各增加多少秒,再用程序验证。这样的改动很小,却能检查你是否真的把“特征、权重和输出”连在了一起。
我们用循环,是为了看清每个数怎样算出来。以后处理大批数据时,会把相同的运算交给数值库完成,省去手写循环,也更方便利用高效的计算实现。
9. 自己动手,把符号变成熟悉的东西
- 对于 ,把输入从 10 改为 12,和把 从 3 改为 4,有什么不同?取 时, 是多少?
- 一份文档有 6 页黑白、3 页彩色,按第一台打印机的参数,预测耗时是多少?如果把这两个特征的位置写反了,却没有调整权重,会得到多少?
- 已知 、,求 、它的长度,以及 与 的逐元素乘积。
- 的形状为 , 为 ,那么 是什么形状?若数据按本章的方式排列,各个数字代表什么?
- 为什么在矩阵 的右边乘 可以计算,却不能直接右乘三个分量的权重向量?
- 取 ,它会把竖直坐标加倍。使用本章的 和 ,分别算 与 。结果相同吗?
- 把脚本中的第一份文档改成
[10, 3]。先猜两台打印机的预测,再运行核对。接着故意让dot([1, 2], [3])运行一次,看看为什么要先检查分量个数。 - 一批图片的形状是
(8, 32, 32, 3),若按本章的轴顺序解释,共存了多少个数字?它有几条轴? - 把 放大十倍, 保持不变。点积、投影长度和余弦相似度各会怎样变化?先看图 3 想一想,再用代码中的
dot和sqrt核对。
展开参考答案与代码提示
- 改 是用同一条规则处理新输入;改 则改变了规则。。
- 正确顺序下, 秒。只交换特征、保留原权重,会得到 秒。分量个数虽然相同,含义却错位了。
- ,长度为 2;逐元素乘积为 ,与点积 7 不同。
- 形状是 。5 表示样本数,3 表示输入特征数,4 表示每个样本要得到的输出数。
- 每一行只有两个特征,无法与三个权重逐项配对。新增特征时,要同时提供数据中相应的一列,并明确新权重的含义。
- ,再乘 得 ;,再乘 得 。两者不同,说明换序会改变结果。
- 第一台增加 5 秒,变为 65 秒;第二台增加 4 秒,变为 62 秒。分量数不等时,
dot会抛出ValueError;如果去掉检查,zip只配对第一项,会把不完整的计算误当作结果。 - 一共有 个数字,按四条轴组织。
- 点积从 7 变为 70,沿 方向的投影长度也扩大十倍,从约 1.94 变为约 19.41。余弦相似度仍约为 0.868,因为分子中的点积和分母中的 同时扩大十倍,两个向量的方向都没变。
可以在脚本旁新建一个文件,补上这段小实验:
from vectors_matrices import TRANSFORM, matmul, matvec
D = [[1, 0], [0, 2]]
p = [1, 2]
print(matvec(TRANSFORM, matvec(D, p))) # [6, 4]
print(matvec(D, matvec(TRANSFORM, p))) # [4, 4]
print(matmul(TRANSFORM, D)) # [[2, 2], [0, 2]]
print(matmul(D, TRANSFORM)) # [[2, 1], [0, 2]]
进阶选做:用循环写一个 transpose 函数,把本章的 矩阵变成 。用已经知道的结果核对,再试着连续转置两次。你应该回到原矩阵,而且每个数字的位置也都恢复了。
10. 下一步,怎样找到更好的参数
一开始,我们只有“页数乘 3,再加 20”。现在,同样的思路已经可以写成一个处理多种特征、多份样本的模型。函数说明怎样从输入得到输出,向量把相关的数组织起来,矩阵让一批相似的计算有了清楚的结构。
接下来,还有一个在导读中暂时靠枚举解决的问题:参数稍微改一点,损失会怎样变?能不能顺着让损失变小的方向走,而不必逐个试遍?
这正是下一章导数、梯度与优化要讨论的问题。继续往前读之前,可以先用本章的代码改改输入和权重,让这些运算真正变得熟悉。
如果想多看一些图形解释,可以从 MIT 线性代数课程的几何入门继续探索。想对照机器学习中常见的符号,则可以选读 Goodfellow、Bengio 和 Courville 所著《深度学习》的第 2 章:线性代数,先看 2.1 和 2.2 节即可;后面的逆矩阵、特征分解等内容可以暂时放下。