原始答案:
我不确定你是否喜欢数学课程通常如何引入矩阵。作为一个程序员,你可能会更高兴地抓到任何像样的三维图形书籍。它当然应该有非常具体的3x3矩阵。还要找出那些能教你的
projective transformations
(射影几何是一个非常漂亮的低维几何区域,易于编程)。
使用python 3的矩阵数学迷你课程
内容:
-
矩阵
[Vector, __add__, reflect_y, rotate, dilate, transform]
-
矩阵:重载
[Matrix, __add__, __str__, __mul__, zero, det, inv, __pow__]
-
奖金:复数
-
矩阵:(r)演化
. 它已经在制作中了(结尾有一个摘要)
前言:
根据我的教学经验,我认为其他人引用的课程非常好
课程
. 这意味着,如果你的目标是像数学家那样理解矩阵,那么你就应该全力以赴地理解整个过程。但是,如果你的目标更为谦虚,下面我将尝试一些更适合你需要的东西(但我的目标仍然是传达许多理论概念,有点与我最初的建议相矛盾)。
如何使用:
-
这篇文章很长。你可以考虑打印这个,然后慢慢地,就像一天打印一部分。
-
代码是必不可少的。这是一门面向程序员的课程。运动也是必不可少的。
-
你应该
take a look at the code companion
其中包含所有这些代码和更多
-
这是“2换1”的特别提示:您也可以在这里学习python 3。和复数。
-
我会高度重视任何阅读这篇文章的尝试(我是否有资格获得有史以来最长的文章?),所以如果你不理解某件事(也如果你理解的话),请随时发表评论。
1。矩阵
向量
在矩阵出现向量之前。你一定知道如何处理二维和三维向量:
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)
然后得到答案
(19, 1)
很好地打印为一个向量(如果例子看起来不熟悉,想想这个代码将如何看C++)。
变换
现在能写都很酷了
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
和
反射X
. 我关心的是,
反射波
无法在不访问的派生类中定义
X
和
Y
而且它仍然有效!
数学家们会称这些函数为
算子
. 他们会这么说的
反射波
是通过
作文
算子的
轻弹
和
反射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)
孩子,它丑吗?
扩张
这些旋转当然是有用的,但它们不是你所需要做的一切,即使是二维图形。考虑以下转换:
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
.
我会用这个
扩张
函数来演示数学家调用的东西
交换性
:也就是说,对于每个参数值
a
,
b
,
c
,
d
你可以肯定
dilate(a, b) ⦠dilate(c, d) = dilate(c, d) ⦠dilate(a, b)
矩阵
让我们总结一下我们在这里的所有东西,
向量上的运算符
X
-
轻弹
,
反射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)
用一些数字和/或参数代替
?
S.
-
例子:
计算出'?'的值是多少?是为了
__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))
-
练习:
元组是什么
m
描述
轻弹
,
膨胀(x,y)
,
旋转(角度)
?
当我们分开的时候
Vector
同学们,下面是一个针对那些想测试向量数学知识和蟒蛇技能的人的练习:
-
最后一场战斗:
添加到
矢量
类所有你能想到的向量运算(你能为向量重载多少个标准运算符)?看看我的答案)。
2。矩阵:重载
正如我们在前一节中发现的,矩阵可以被认为是一个简写,它允许我们以一种简单的方式对向量运算进行编码。例如,
rotation_90_matrix
将旋转编码为90度。
矩阵对象
现在,当我们把注意力从向量转移到矩阵上时,我们无论如何都应该有一个类
也适用于矩阵。而且,在这个函数中
Vector.transform(...)
上面的矩阵的作用有点被曲解了。这是比较平常的
米
当向量改变时,我们要固定,从现在开始,我们的变换将是矩阵类的方法:
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))
加法
现在,我们来看看我们还能用矩阵做什么。记住那个矩阵
米
实际上只是对向量进行运算编码的一种方法。注意,对于两个功能
m1(x)
和
m2(x)
我可以创建一个新函数(使用
lambda notation
)
m = lambda x: m1(x) + m2(x)
. 结果是
m1
和
m2
是用矩阵编码的,
您也可以对此进行编码
米
利用矩阵
!
你只需要添加它的数据,比如
(0, 1, -1, 0) + (0, 1, -1, 0) = (0, 2, -2, 0)
. 下面是如何在python中添加两个元组,以及一些非常有用和高度python技术:
def __add__(self:'matrix', snd:'another matrix'):
"""This will add two matrix arguments.
snd is a standard notation for 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)
乘法
对于一个参数函数,我们可以做的最重要的事情是组合它们:
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[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)
现在让我说实话:这个
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))
看看为什么它被称为
对角线的
矩阵。
由于以前发现运算的组成并不总是可交换的,
*
对于矩阵,运算符也不总是可交换的。
-
练习:
返回并刷新
交换性
如果需要的话。然后给出矩阵的例子
A
,
B
,由
R
和
diag
,
这样
A*B
不等于
B * A
.
这有点奇怪,因为数字的乘法总是可交换的,这就提出了一个问题,即
组成
真的应该被召唤
__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
-
练习:
证明这些陈述。如何定义
A—B
依据
+
,
*
和
迪亚格
?什么?
A - A
等于?添加方法
__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
关于一些矩阵
一
向量
乙
.
-
练习:
找到
一
和
乙
这样一个旋转
pi/2
围绕这一点
(1, 0)
上面有表格。它们是独一无二的吗?
注意,对于每个向量都有一个仿射变换,它是
转移
通过向量。
仿射变换可以拉伸或扩张形状,但在任何地方都应该以同样的方式进行。现在我希望你相信任何图形的面积在变换下都会变为一个常数。对于矩阵给出的变换
一
这个共同点被称为
行列式
属于
一
并且可以通过将面积公式应用于两个向量来计算
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
.
-
练习:
看看这个。当参数之一为0时会发生什么?什么时候是阴性?
如你所见,旋转矩阵的行列式总是相同的:
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]
,凌晨二点
1
加上(如果有疑问,在纸上做这个)来解决
v.x
.
如果你真的尝试过,你应该
A.det() * v.x = (A.m[3]) * b.x + (-A.m[1]) * b.y
,这意味着你可以
v
乘法
乙
通过其他矩阵。这个矩阵叫做
逆
属于
一
:
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.inv()
总是等于
one
-
练习:
你自己检查一下。从逆矩阵的定义解释为什么会这样。
这就是为什么数学家表示
A.inv()
通过
一
- 1
. 我们写一封信怎么样
很好的功能
A ** n
符号表示法
一
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 for 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 = A
)
-
练习:
求…的平方根
diag(-1, -1)
. 这是唯一可能的答案吗?
找一个矩阵的例子
不
有一个平方根。
奖金:复数
在这里,我将在一个部分向您介绍这个主题!
因为这是一个复杂的主题,我可能会失败,所以请提前原谅我。
首先,类似于我们的矩阵
zero
和
一
我们可以用任何实数做一个矩阵
diag(number, number)
. 这种形式的矩阵可以加、减、乘、反,结果会模仿数字本身的情况。因此,就所有实际目的而言,可以这样说,例如,
diag(5, 5)
是
5。
然而,python还不知道如何处理表单的表达式
A + 1
或
5 * B
哪里
一
和
乙
是矩阵。如果你感兴趣,你应该尽一切可能去做下面的练习或者看看我的实现(它使用了一个很酷的python特性
装饰者
)否则,只需知道它已经实现。
-
古鲁的练习:
更改a中的运算符
矩阵
类,以便在其中一个操作数是矩阵,另一个操作数是数字的所有标准操作中,该数字自动转换为
迪亚格
矩阵。还要添加相等比较。
下面是一个示例测试:
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
复数
.
因为这些仍然是我们
矩阵
类中,我们可以使用这些操作执行很多操作:加法、减法、乘法、除法、幂运算-它们都已经实现了!矩阵不是很神奇吗?
我忽略了如何打印操作结果的问题
E = (1 + 2*J) * (1 + 3*J)
所以它看起来像
J
而不是
2x2
矩阵。如果你仔细检查,
您将看到您需要以这种格式打印矩阵的左列
... + ...J
(还有一件很好的事:它确实是
E(x_axis)
!)那些知道两者区别的人
str()
和
repr()
应该看到命名一个函数会产生如下形式的表达式是很自然的
RePrP()
.
-
练习:
编写函数
Matrix.__repr__
就这样,然后用它做一些测试,比如
(1 + J) ** 3
首先在纸上计算结果,然后用python进行尝试。
-
数学问题:
决定因素是什么
A+B*J
?如果你知道
绝对值
复数的意义是:它们是如何连接的?绝对值是多少
一
?属于
a*J
?
三。矩阵:(r)演化
在这三部曲的最后一部分,我们将看到一切都是矩阵。我们先从将军开始
M x N
矩阵,并找出如何将向量视为
1 x N
矩阵以及为什么数字与对角矩阵相同。作为旁注,我们将研究复数
2 x 2
矩阵。
最后,我们将学习使用矩阵编写仿射和射影变换。
所以计划的课程是
[MNMatrix, NVector, Affine, Projective]
.
我想如果你能忍受我直到这里,你可能对这个续集感兴趣,所以我想听听我是否应该继续这个(在哪里,因为我非常确定我超过了一个文件的合理长度)。