最近我一直在从坐标渲染图表和图形中获得乐趣,并且对使用矩阵来转换坐标空间非常着迷。
我已经成功地缩放和反转了二维坐标空间,但现在我的兴趣被激发了。
我想知道在哪里可以找到关于矩阵、矩阵数学的清晰、信息量大的(免费)教育材料,特别是与二维和三维空间相关的材料?
最近我一直在从坐标渲染图表和图形中获得乐趣,并且对使用矩阵来转换坐标空间非常着迷。
我已经成功地缩放和反转了二维坐标空间,但现在我的兴趣被激发了。
我想知道在哪里可以找到关于矩阵、矩阵数学的清晰、信息量大的(免费)教育材料,特别是与二维和三维空间相关的材料?
原始答案:我不确定你是否会喜欢数学课程通常如何介绍矩阵。作为程序员,你可能更喜欢阅读任何一本不错的三维图形书籍。它肯定会有非常具体的3x3矩阵。此外,找出那些将教你projective transformations(投影几何是低维几何中非常美丽且易于编程的领域)的书籍。
目录:
[向量,__add__,reflect_y,rotate,dilate,transform]
[矩阵,__add__,__str__,__mul__,zero,det,inv,__pow__]
前言:基于我的教学经验,我认为其他人提到的课程都是非常好的课程。这意味着如果你的目标是像数学家一样理解矩阵,那么你应该全盘接受整个课程。但是,如果你的目标更为谦虚,那么这里是我尝试根据你的需求量身定制的内容(但仍旧旨在传达许多理论概念,有点与我的原始建议相矛盾)。
使用方法:
在矩阵之前是向量。您肯定知道如何处理二维和三维向量:
class Vector:
"""This will be a simple 2-dimensional vector.
In case you never encountered Python before, this string is a
comment I can put on the definition of the class or any function.
It's just one of many cool features of Python, so learn it here!
"""
def __init__(self, x, y):
self.x = x
self.y = y
v = Vector(5, 3)
w = Vector(7, -1)
但是它本身并不太有趣。让我们添加更多有用的方法:
def __str__(self: 'vector') -> 'readable form of vector':
return '({0}, {1})'.format(self.x, self.y)
def __add__(self:'vector', v: 'another vector') -> 'their sum':
return Vector(self.x + v.x, self.y + v.y)
def __mul__(self:'vector', number: 'a real number') -> 'vector':
'''Multiplies the vector by a number'''
return Vector(self.x * number, self.y * number)
这使事情变得更加有趣,因为我们现在可以写成:
print(v + w * 2)
现在能够写出1274 * w
是很酷的,但是你需要更多的向量操作来进行图形处理。以下是其中一些:可以将向量围绕(0,0)
点翻转,可以将其围绕x
轴或y
轴反射,可以顺时针或逆时针旋转(在这里画个图是个好主意)。
让我们做一些简单的操作:
...
def flip(self:'vector') -> 'vector flipped around 0':
return Vector(-self.x, -self.y)
def reflect_x(self:'vector') -> 'vector reflected around x axis':
return Vector(self.x, -self.y)
print(v.flip(), v.reflect_x())
flip(...)
?那么reflect_x
呢?现在你可能会想为什么我省略了reflect_y
。好吧,这是因为我希望你停下来,写下你自己的版本。好的,这是我的版本:
def reflect_y(self:'vector') -> 'vector reflected around y axis':
return self.flip().reflect_x()
看,如果你观察这个函数的计算方式,它实际上非常简单。但是突然发生了一件惊人的事情:我能够仅使用现有的转换flip
和reflect_x
来编写一个变换。对于所有的事情,reflect_y
可以在派生类中定义而不需要访问x
和y
,它仍然可以工作!
数学家会将这些函数称为运算符。他们会说reflect_y
是通过运算符flip
和reflect_x
的组合获得的运算符,表示为reflect_y = flip ○ reflect_x
(你应该看到小圆圈,一个Unicode符号25CB
)。
=
符号来表示两个操作产生相同的结果,就像上面的段落中那样。这是一个"数学上的=
",不能被表达为程序。所以如果我这样做
print(v.reflect_y())
我得到了结果(-5, 3)
。去画出来!
reflect_y ◦ reflect_y
。你会如何命名它?这些操作都很好,很有用,但你可能想知道为什么我这么慢才介绍旋转。好的,我来了:
def rotate(self:'vector', angle:'rotation angle') -> 'vector':
??????
如果您知道如何旋转向量,那么您可以继续填写问号。否则,请再忍耐一下,看一个更简单的情况:逆时针旋转 90
度。这个不难在纸上画出来:
def rotate_90(self:'vector') -> 'rotated vector':
new_x = - self.y
new_y = self.x
return Vector(new_x, new_y)
x_axis = Vector(1, 0)
y_axis = Vector(0, 1)
print(x_axis.rotate_90(), y_axis.rotate_90())
现在会给出 (0, 1) (-1, 0)
。请自己运行代码!
flip = rotate_90 ◦ rotate_90
。无论如何,我不再隐藏秘密配方:
import math # we'll need math from now on
...
class Vector:
...
def rotate(self:'vector', angle:'rotation angle') -> 'rotated vector':
cos = math.cos(angle)
sin = math.sin(angle)
new_x = cos * self.x - sin * self.y
new_y = sin * self.x + cos * self.y
return Vector(new_x, new_y)
print(x_axis.rotate(90), y_axis.rotate(90))
(0, 1) (-1, 0)
,那么你注定会失望。该代码会打印出:(-0.448073616129, 0.893996663601) (-0.893996663601, -0.448073616129)
哇塞,它真的很丑!
注释: 我们在上面的例子中对x
应用了操作rotate(90)
。我们得到的知识是rotate(90) != rotate_90
。
问题:这里发生了什么?如何用rotate
来表示rotate_90
?如何用rotate
来表示flip
?
这些旋转肯定很有用,但它们并不是你需要做的所有2D图形。考虑以下变换:
def dilate(self:'vector', axe_x:'x dilation', axe_y:'y dilation'):
'''Dilates a vector along the x and y axes'''
new_x = axe_x * self.x
new_y = axe_y * self.y
return Vector(new_x, new_y)
这个 dilate
函数会以可能不同的方式扩张 x
和 y
轴。
dilate(?, ?) = flip
, dilate(?, ?) = reflect_x
中填入问号。我将使用这个 dilate
函数来演示数学家所称的“交换律”:也就是说,对于参数 a
、b
、c
、d
的每个值,您都可以确信
dilate(a, b) ◦ dilate(c, d) = dilate(c, d) ◦ dilate(a, b)
Exercise: Prove it. Also, is it true that for all possible values of parameters those below would hold?
`rotate(a) ◦ rotate(b) = rotate(b) ◦ rotate(a)`
`dilate(a, b) ◦ rotate(c) = rotate(c) ◦ dilate(a, b)`
`rotate(a) ◦ __mul__(b) = __mul__(b) ◦ rotate(a)`
让我们总结一下我们这里的所有内容,我们针对向量 x
的 操作符
flip
,reflect_x
,*
,rotate(angle)
,dilate(x, y)
通过这些操作符,我们可以做出一些非常疯狂的东西,比如
flip ◦ rotate(angle) ◦ dilate(x, y) ◦ rotate(angle_2) ◦ reflect_y + reflect_x = ???
随着您创建越来越复杂的表达式,希望能有某种顺序,使得所有可能的表达式突然变成一个有用的形式。别担心!神奇的是,上面的每个表达式都可以简化为
def ???(self:'vector', parameters):
'''A magical representation of a crazy function'''
new_x = ? * self.x + ? * self.y
new_y = ? * self.x + ? * self.y
return Vector(new_x, new_y)
使用一些数字和/或参数代替?
。
__mul__(2) ◦ rotate(pi/4)
中'?'的值dilate(x, y) ◦ rotate(pi/4)
中'?'的值这使我们能够编写通用函数
def transform(self:'vector', m:'matrix') -> 'new vector':
new_x = m[0] * self.x + m[1] * self.y
new_y = m[2] * self.x + m[3] * self.y
return Vector(new_x, new_y)
该程序将接收任何四元组数字,称为矩阵,并将其应用于向量x
。以下是一个示例:
rotation_90_matrix = (0, -1, 1, 0)
print(v, v.rotate_90(), v.transform(rotation_90_matrix))
这段代码输出 (5, 3) (-3, 5) (-3, 5)
。注意,如果你对原点应用任何矩阵的 transform
,你仍然会得到原点:
origin = Vector(0, 0)
print(origin.transform(rotation_90_matrix))
flip
、dilate(x, y)
和rotate(angle)
的元组m
是什么?当我们与Vector
类分别时,这里有一个练习,供那些想要测试他们的向量数学知识和Python技能的人使用:
Vector
类添加所有你能想到的向量操作(你可以重载多少个标准运算符来处理向量?查看我的答案)。正如我们在前一节中发现的那样,矩阵可以被视为一种简便的方式,它允许我们以简单的方式编码向量操作。例如,rotation_90_matrix
编码了90度旋转。
现在,当我们将注意力从向量转移到矩阵时,我们应该尽一切努力也为矩阵拥有一个类。此外,在上面的Vector.transform(...)
函数中,矩阵的角色有些被误解了。通常情况下,m
是固定的,而向量则会发生变化,因此从现在开始,我们的转换将成为矩阵类的方法:
class Matrix:
def __init__(self:'new matrix', m:'matrix data'):
'''Create a new matrix.
So far a matrix for us is just a 4-tuple, but the action
will get hotter once The (R)evolution happens!
'''
self.m = m
def __call__(self:'matrix', v:'vector'):
new_x = self.m[0] * v.x + self.m[1] * v.y
new_y = self.m[2] * v.x + self.m[3] * v.y
return Vector(new_x, new_y)
__call__
重载了矩阵中 (...)
的含义,因此我可以使用标准符号来表示一个矩阵对向量的作用。另外,矩阵通常使用单个大写字母来表示:J = Matrix(rotation_90_matrix)
print(w, 'rotated is', J(w))
现在,让我们看看矩阵还能做什么。请记住,矩阵m
实际上只是一种对向量进行编码的方法。请注意,对于两个函数m1(x)
和m2(x)
,我可以使用lambda notation创建一个新函数m = lambda x: m1(x) + m2(x)
。事实证明,如果m1
和m2
由矩阵编码,您也可以使用矩阵来编码这个m
!
您只需添加其数据,例如(0, 1, -1, 0) + (0, 1, -1, 0) =(0, 2, -2, 0)
。以下是如何使用一些非常有用且高度Pythonic的技术在Python中添加两个元组的方法:
def __add__(self:'matrix', snd:'another matrix'):
"""This will add two matrix arguments.
snd is a standard notation for the second argument.
(i for i in array) is Python's powerful list comprehension.
zip(a, b) is used to iterate over two sequences together
"""
new_m = tuple(i + j for i, j in zip(self.m, snd.m))
return Matrix(new_m)
J + J
或甚至J + J + J
,但要查看结果,我们必须弄清楚如何打印矩阵。一种可能的方法是打印一个由四个数字组成的4元组,但让我们从Matrix.__call__
函数中得到提示,这些数字应该组织成一个2x2
块: def as_block(self:'matrix') -> '2-line string':
"""Prints the matrix as a 2x2 block.
This function is a simple one without any advanced formatting.
Writing a better one is an exercise.
"""
return ('| {0} {1} |\n' .format(self.m[0], self.m[1]) +
'| {0} {1} |\n' .format(self.m[2], self.m[3]) )
如果你观察这个函数的运行,你会注意到还有改进的空间:
print((J + J + J).as_block())
Matrix.__str__
,它将四舍五入数字并以固定长度的字段打印出来。现在您应该能够编写旋转矩阵:
def R(a: 'angle') -> 'matrix of rotation by a':
cos = math.cos(a)
sin = math.sin(a)
m = ( ????? )
return Matrix(m)
Exercise: Examine the code for Vector.rotate(self, angle)
and fill in the question marks. Test with
from math import pi
print(R(pi/4) + R(-pi/4))
使用单参数函数最重要的事情是组合它们:f = lambda v: f1(f2(v))
。如何通过矩阵来实现这一点呢?这需要我们研究 Matrix(m1) ( Matrix(m2) (v))
的工作原理。如果你展开它,你会注意到
m(v).x = m1[0] * (m2[0]*v.x + m2[1]*v.y) + m1[1] * (m2[2]*v.x + m2[3]*v.y)
同样地,对于m(v).y
,如果你打开括号,看起来非常类似于使用新元组m
的Matrix.__call__
,使得m[0] = m1[0] * m2[0] + m1[2] * m2[2]
。因此,让我们将其作为一个新定义的提示:
def compose(self:'matrix', snd:'another matrix'):
"""Returns a matrix that corresponds to composition of operators"""
new_m = (self.m[0] * snd.m[0] + self.m[1] * snd.m[2],
self.m[0] * snd.m[1] + self.m[1] * snd.m[3],
???,
???)
return Matrix(new_m)
Exercise: Fill in the question marks here. Test it with
print(R(1).compose(R(2)))
print(R(3))
Math exercise: Prove that R(a).compose(R(b))
is always the same as R(a + b)
.
compose
函数实际上是数学家决定如何乘法矩阵的方式。这种表示法有意义:A * B
是描述运算符A ○ B
的矩阵,正如我们接下来将看到的那样,称此为“乘法”的原因更深。
要在Python中开始使用乘法,我们只需在Matrix
类中进行排序即可:
class Matrix:
...
__mul__ = compose
(R(pi/2) + R(pi)) * (R(-pi/2) + R(pi))
。请先在纸上尝试自行找出答案。+
和*
的规则让我们为与dilate(a, b)
运算符对应的矩阵取一个好名字。现在D(a, b)
没有问题,但我会利用这个机会介绍一种标准符号:
def diag(a: 'number', b: 'number') -> 'diagonal 2x2 matrix':
m = (a, 0, 0, b)
return Matrix(m)
尝试使用print(diag(2, 12345))
查看为什么它被称为对角线矩阵。
由于操作的组合之前被发现并不总是可交换的,对于矩阵,*
运算符也不总是可交换的。
R
和diag
制成的矩阵A
,B
的示例,
使得A * B
不等于B * A
。这有点奇怪,因为数字的乘法总是可交换的,这引出了一个问题,即compose
是否真的应该被称为__mul__
。以下是+
和*
确实满足的许多规则:
A + B = B + A
A * (B + C) = A * B + A * C
(A + B) * C = A * C + B * C
(A * B) * C = A * (B * C)
A - B
的操作,(A - B) + B = A
+
、*
和diag
定义A - B
?A - A
等于什么?在类Matrix
中添加方法__sub__
。如果计算R(2) - R(1)*R(1)
会发生什么?它应该等于什么?(A * B) * C = A * (B * C)
等式被称为结合律,特别好的是它意味着我们不必担心在形如A * B * C
的表达式中放置括号:
print(R(1) * (diag(2,3) * R(2)))
print((R(1) * diag(2,3)) * R(2))
让我们寻找常规数字0
和1
以及减法的类比:
zero = diag(0, 0)
one = diag(1, 1)
以下是易于验证的规则:
A + zero = A
A * zero = zero
A * one = one * A = A
这些规则被称为环公理,意味着规则已经完备。数学家会说矩阵形成一个环,并且他们在讨论环时总是使用符号+
和*
,我们也一样。
使用这些规则,可以很容易地计算出上一节中的表达式:
(R(pi/2) + R(pi)) * (R(-pi/2) + R(pi)) = R(pi/2) * R(-pi/2) + ... = one + ...
(R(a) + R(b)) * (R(a) - R(b)) = R(2a) - R(2b)
。现在我们回到矩阵的定义方式:它们是向量可以进行的一些操作的快捷方式,因此您可以实际绘制出来。您可能需要拿起笔或查看其他人建议的材料以查看不同平面变换的示例。
在这些变换中,我们将寻找仿射变换,即在任何地方都“相同”的变换(没有弯曲)。例如,围绕某个点(x,y)
旋转就符合条件。现在这个变换不能表示为lambda v:A(v)
,但可以写成形式lambda v:A(v)+b
,其中A
是某个矩阵,b
是某个向量。
A
和b
,使得围绕点(1,0)
旋转pi/2
具有上述形式。它们唯一吗?请注意,对于每个向量,都存在一个仿射变换,它是向量的平移。
仿射变换可以拉伸或扩大形状,但必须在任何地方以相同的方式进行。现在我希望您相信,在变换下,任何图形的面积都会按照一个常数改变。对于由矩阵A
给出的变换,这个系数被称为行列式,可以应用面积公式计算两个向量A(x_axis)
和A(y_axis)
:
def det(self: 'matrix') -> 'determinant of a matrix':
return self.m[0]*self.m[3] - self.m[1] * self.m[2]
diag(a, b).det()
等于a * b
。
from random import random
r = R(random())
print (r, 'det =', r.det())
det
的一个有趣之处在于它是乘性的(如果你冥想足够长时间,这种特性从定义中就可以得出):
A = Matrix((1, 2, -3, 0))
B = Matrix((4, 1, 1, 2))
print(A.det(), '*', B.det(), 'should be', (A * B).det())
使用矩阵的一个有用方法是编写两个线性方程组成的系统
A.m[0]*v.x + A.m[1]*v.y = b.x
A.m[2]*v.x + A.m[3]*v.y = b.y
简单来说:A(v) = b
。让我们按照(某些)高中教学的方式解决这个系统:将第一个方程乘以A.m[3]
,第二个方程乘以-A.m1
,然后相加(如果不确定,请在纸上操作)以解出v.x
。
如果你真的尝试过,你应该得到A.det() * v.x = (A.m[3]) * b.x + (-A.m[1]) * b.y
,这表明你总是可以通过将b
乘以另一个矩阵来获得v
。这个矩阵被称为A的逆矩阵:
def inv(self: 'matrix') -> 'inverse matrix':
'''This function returns an inverse matrix when it exists,
or raises ZeroDivisionError when it doesn't.
'''
new_m = ( self.m[3] / self.det(), -self.m[1] / self.det(),
????? )
return Matrix(new_m)
正如你所看到的,当矩阵的行列式为零时,这种方法会发出明显的失败声音。如果你真的想要,你可以使用以下方式捕获此异常:
try:
print(zero.inv())
except ZeroDivisionError as e: ...
self.det() == 0
时,证明逆矩阵不存在。编写分割矩阵的方法并进行测试。使用逆矩阵解方程A(v) = x_axis
(A
如上所述)。逆矩阵的主要属性是A * A.inv()
始终等于one
这就是为什么数学家用A
-1来表示A.inv()
。我们可以写一个很好的函数,使用A ** n
符号来表示A
n吗?请注意,天真的for i in range(n): answer *= self
循环的时间复杂度为O(|n|),这肯定太慢了,因为可以用log |n|
的复杂度完成:
def __pow__(self: 'matrix', n:'integer') -> 'n-th power':
'''This function returns n-th power of the matrix.
It does it more efficiently than a simple cycle. A
while loop goes over all bits of n, multiplying answer
by self ** (2 ** k) whenever it encounters a set bit.
...
练习:填写此函数中的详细信息。使用以下测试:
X,Y = A ** 5,A ** -5
print(X,Y,X * Y,sep ='\n')
这个函数仅适用于整数值的n
,即使对于某些矩阵,我们也可以定义分数幂,例如平方根(换句话说,一个矩阵B
,使得B * B = A
)。
diag(-1,-1)
的平方根。 这是唯一可能的答案吗?
找到一个没有平方根的矩阵的例子。在这里,我将在正好一个章节中向您介绍这个主题! 由于这是一个复杂的主题,我可能会失败,请提前原谅我。
首先,类似于我们拥有矩阵zero
和one
,我们可以通过执行diag(number,number)
来将任何实数制成矩阵。该形式的矩阵可以添加,减去,乘以,反转,并且结果会模拟数字本身的情况。因此,从实际意义上讲,可以说例如diag(5,5)
等于5。
A + 1
或 5 * B
的表达式,其中 A
和 B
是矩阵。如果您感兴趣,可以尝试进行以下练习或查看我的实现(它使用了一个很酷的 Python 特性称为“装饰器”);否则,只需知道它已被实现即可。
Matrix
类中的运算符,使得在所有标准操作中,其中一个操作数是矩阵,另一个操作数是数字时,数字会自动转换为 diag
矩阵。此外,添加一个相等比较。这里是一个示例测试:
print( 3 * A - B / 2 + 5 )
现在介绍第一个有趣的复数:矩阵J
,在一开始就被引入并等于Matrix((0, 1, -1, 0))
,具有一个有趣的属性,即J * J == -1
(试试看!)。这意味着J
肯定不是普通的数字,但正如我刚才说的,矩阵和数字很容易混合在一起。例如,
(1 + J) * (2 + J) == 2 + 2 * J + 1 * J + J * J = 1 + 3 * J
使用之前列出的规则。如果我们在Python中测试,会发生什么?
(1 + J) * (2 + J) == 1 + 3*J
这应该会愉快地显示True
。另一个例子:
(3 + 4*J) / (1 - 2*J) == -1 + 2*J
a + b*J
的表达式称为复数。因为这些仍然是我们的Matrix
类的实例,所以我们可以对它们进行许多操作:加法、减法、乘法、除法、幂运算 - 这一切都已经实现了!矩阵不是很神奇吗?E = (1 + 2*J) * (1 + 3*J)
这样的操作结果,使其看起来像一个带有J
的表达式,而不是一个2x2
矩阵。如果你仔细观察,你会发现你需要以... + ...J
的格式打印该矩阵的左列(还有一个好消息:它恰好是E(x_axis)
!)那些知道str()
和repr()
之间区别的人应该自然而然地将产生这种形式表达式的函数命名为repr()
。
练习: 编写函数Matrix.__repr__
,并尝试一些测试,例如(1 + J) ** 3
,先在纸上计算结果,然后用Python尝试。
数学问题: a + b*J
的行列式是多少?如果你知道一个复数的绝对值是什么:它们之间有什么联系?a
的绝对值是多少?a*J
的绝对值是多少?
在这个三部曲的最后一部分中,我们将看到一切都是矩阵。我们将从一般的M x N
矩阵开始,发现向量可以被视为1 x N
矩阵,以及为什么数字与对角线矩阵相同。顺便说一句,我们将探讨复数作为2 x 2
矩阵的方式。
最后,我们将学习使用矩阵编写仿射和投影变换。
因此,计划的类是[MNMatrix,NVector,Affine,Projective]
。
我猜如果你能一直忍受到这里,你可能会对这个续集感兴趣,所以我想听听是否应该继续这个话题(以及在哪里,因为我相当确定我已经超出了单个文档的合理长度)。
麻省理工学院有许多数学课程的材料可以在网上找到,网址为http://ocw.mit.edu/OcwWeb/Mathematics/。一旦你掌握了基础知识,他们也提供物理笔记在线查阅。
对于初学者来说,最好的书籍之一是Carl Meyer的“Matrix Analysis and Applied Linear Algebra”。
你可以在这里在线查看整本书(尽管它带有版权水印): http://www.matrixanalysis.com/DownloadChapters.html
你可能想看一下第5章第326-332页,其中涵盖了三维计算机图形中的旋转。
P.S. 如果坐标变换是你的事情,那么你完成线性代数后可能会对微分几何感兴趣。
在 Google 图书 中搜索 "矩阵",会得到很多讲座,其中一些直接与变换相关 - this 是最早的结果之一,但我鼓励您查看更多。
我也鼓励(我不知道这是否是正确的词,我只是在学习英语)你,在这些书籍中寻找此类信息(虽然它们并不是免费的,但你可以在 Google 图书中找到很多旧版本的内容):
每一本书都有关于数学宝石的章节,里面有很多巧妙的技巧。这些书值得每一分钱。
还有GPU编程宝石,您也可以尝试一下。
练习:
如果我找到更多的链接,我会编辑并在此处添加链接,但说实话 - 我在使用谷歌约10分钟后就找到了这些链接。世界上最受欢迎的浏览器存储有关所有内容的数据 - 是的,“所有内容”也包括矩阵。
干杯,伙计。
我认为你应该花几天时间学习在三维空间中向量的点积和叉积。然后学习三角函数和向量之间的关系。这样,矩阵就会更容易理解了。