数组的提取操作

在数据处理的工程中,经常会涉及到数组提取的操作。很多时候都需要从一个大数组中按维度,按长度等提取出一个小数组。本文将总结一些常用的提取操作。

用在方括号提取数组

对于tensor, pytorch, numpy这些拥有数组处理能力的库而言,其本身具备很强大的数据处理功能。大部分的数据提取操作都可以通过方括号操作符完成数据提取的任务。我们以pytorch为例,pytorch的数组方括号操作符中支持3类对象:

  1. 使用冒号: 描述某个维度中提取的范围。例如a[0:4, 4: -1]
  2. 使用一个整数数字指定某个维度中的特定项目。例如a[2, 8]
  3. 使用一个可迭代的整数队列指定某个维度中的一几个或者一个特定的项目。例如a[[0,1,2], [0,1,5]]

这3类对象在同一个提取操作中可以同时存在。这里,前2个提取操作都非常好理解。而3个提取操作会有一些麻烦。

冒号的应用和python常规中常规操作一样,a[:10, 4:7]表示对a数组的0维度,提取从0开始到第10项结尾的数据。在第1维度,提取从第4项开始到第7项结尾的数据。

单个整数数字表示对某个维度提取指定的某个元素。采用这样的提取方法,得到的数组会比原始数组减少一个维度。例如:

>>> a
tensor([[0., 1., 2.],
        [1., 3., 3.]])
>>> a[:,1]
tensor([1., 3.])
>>> a.shape
torch.Size([2, 3])
>>> a[:,1].shape
torch.Size([2])

上面例子可以看到a[:,1]变成了1维数组。

使用可迭代的整数队列指定项目提取时,情况就要复杂一些。它有以下几条规则:

  1. 在方括号中所有采用迭代对象做提取应用的,所有迭代对象必须拥有同样的长度,或者长度维1。
  2. 如果迭代对象的最大长度不是1, 而所有迭代对象中存在长度为1的。则长度为1的迭代对象在应用中将会被看做迭代对象长度为那个统一的最大长度。且,其数值全部为其原始整数的拷贝。例如a[[0], [1,2,3]]将被视作a[[0,0,0], [1,2,3]]。关于这一点,我们可以通过代码轻松验证:
>>> b = torch.tensor([[[0,1,2], [3,4,5]],[[6,7,8], [9,10,11]]])
>>> b[[0], [0,1], [0,1]]
tensor([0, 4])
>>> b[[0], :, [0,1]]
tensor([[0, 3],
        [1, 4]])

在这里b[[0], :, [0,1]]显然被当作了b[[0,0], : , [0,1]]来运算。然后又会被当作[b[0,:,0], b[0,:,1]]来看待。这里我们又要注意一个非常重要的点了。这个例子中前面的[0,0]的意义在于在第0维度选择两个相同的项目,后面的[0,1]则并不是每次都在第2维度选择第0个和第1两个项目的意思。后面的[0,1]的意义是在前面对应的第0个项目中在第2维度选择第0个项目,然后在前面对应的第1个项目中在第2维度选择第1个项目。它并没有在前面第0个项目中选择第0和第1,而是只选择0。这里就注意了,由于第2个迭代选择器选择的都是唯一项目,因此这就会发生维度的减少。例如前面的例子中b本来是2*2*3,总共3个维度的数据,而b[[0,0], :, [0,1]]变成了2*2两个维度的数据。这一切都因为后面的[0,1]选择了指定的一个数据(0只对应0, 1也只对应另外一个0),这一点就让b数组的最后一个维度发生了坍缩。

如果一个提取操作使用了n个迭代选择器,m个单1整数选择器,而原来数组的维度如果是t,则提取之后的数组的维度为 t - m - n + 1。

维度坍缩总结起来就是,如果遇见在某一个维度上只作整数式选择一个特定项目时,该维度就会发生坍缩。例如a[0:10, 4:5]在第1维度上4:5是选择了一个项目,但这里是作为数组来选择一个项目的,因此第1维度不会坍缩。但是如果a[0:10, 4]那就会发生坍缩了。然后,如果选择器中存在超过1个的迭代选择器,那么从第2个迭代选择器开始,每一个迭代选择器都等价于在前面的基础上只做整数式的选择。例如a[[0,1], [2,3]]。前面的[0,1]仍然是做数组式的选择,如果其后面没有[2,3],那么[0,1]几乎等价与0:2的效果。这里后面的[2,3]就不再是做数组式选择了,而是整数式地对前面的0, 做整数2的选择,对前面的1,做整数3的选择。

按步距提取

上文中提到过的提取方式,其实还有一个进一步的增强。那就是按照步距提取数据。方括号中的冒号可以再增加一个,然后第3个参数表示步距值。例如:

# array[start:end:step]
a = [1,2,3,4,5]
a[::2] = [1,3,5]
a[::-2] = [5,3,1]

#以上操作换成numpy.array也是一样的效果

步距的引入,可以进一步增强使用方括号做数据的提取操作。

不同维度间数组的运算

在很多人工智能或者数据处理的流程中,我们会经常遇见大量的数组处理问题。很多时候我们需要处理不同维度的数组,或者做改变数组维度等操作。一个优秀的工程师,尤其是一个愿意沉浸在数据变化过程中的工程师必须熟练的掌握处理不同维度的数组之间运算的问题。

在实际应用中,很多数据操作都会改变数组的维度。例如对3维数组的最后一维度做求和操作,则会把原来的数组变成2维的。

>>> a = torch.tensor([[[1,2,3]], [[2,3,4]]])
>>> a.sum(-1)
tensor([[6],
        [9]])
>>> a.sum(-1).shape
torch.Size([2, 1])
>>> a.shape
torch.Size([2, 1, 3])

然后,我们又会有很多运算会对原维度的数据和改变维度后的数组之间做四则运算。例如,我们想让a数组中最后一个维度的数据,每一个都减去其当前维度的和。比如说,我们想让torch.tensor([[[1,2,3]], [[2,3,4]]])变成 torch.tensor([ [[1-6, 2- 6, 3 - 6]], [[2-9, 3-9, 4-9]] ])。那我们里所当然想尝试这么做:

>>> a
tensor([[[1, 2, 3]],
        [[2, 3, 4]]])
>>> a - torch.tensor([6,9])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: The size of tensor a (3) must match the size of tensor b (2) at non-singleton dimension 2

那么,我们看到,这么直观的让a - [6,9]是不行的。要准确的实现我们的运算想法,就必须得先充分理解不同数组之间做运算的机制。

  1. 一般情况下,不同维度数量,或者同维度的长度不一样的数组之间是不能做四则运算的。标准的数组之间的四则运算只能运行在具备同样维度数量,且每个相对应维度的长度都一样长的数组之间进行。
  2. 不同维度,不同维度长度之间的数组之间只能在各自分别执行自动适配后,双双能满足1的条件后,它们才能达成运算条件。
  3. 数组的自动适配分两个步骤,第一,自动统一维度数量,这个步骤由维度数量少的数组,按最后一个维度对齐,然后再在前面补维度来与维度数量大的数组对齐;第二,在维度数量对齐后,分别对每个维度做长度的对齐操作,如果某个维度上它们长度相同,则看作已经对齐。如果长度比较短的那个数组在该维度的长度为1,则对该数字在该维度上做维度长度的扩张操作,且补齐的数据为那1个数据的拷贝。如果长度较短的那个数组在该维度的长度不为1,则视为此对数组无法自动适配。维度长度的对齐操作是从最后一个维度开始到第0维度结尾的顺序执行。

以上这段定义,可能主要是自动适配的描述不方便理解。下面举例说明:

>>> a
tensor([[[1, 2, 3]],
        [[2, 3, 4]]])
>>> b = torch.tensor([6,9])
>>> a - b

执行a - torch.tensor([6,9])的操作时,首先要让数组的维度数量对齐。a 的shape为[2,1,3], b的shape为[2], 于是首先b的shape会补充为[1,1,2], 即b变成[[[6,9]]]。然后,从最后一个维度开始做维度的长度对齐。b最后一个维度的长度为2, a为3。因此,a, b是无法自动适配了。因为有这条规定"如果长度较短的那个数组在该维度的长度不为1,则视为此对数组无法自动适配。"。因此,我们要想a - b能够执行,则需要b在完成第一步维度数量扩展后的shape为[1,1,1]或者[1,1,3]。我们改变下b的形态:

>>> b = torch.tensor([[6], [9]])
>>> a - b
tensor([[[-5, -4, -3],
         [-8, -7, -6]],

        [[-4, -3, -2],
         [-7, -6, -5]]])

我们这样改变以后,a - b能够顺利执行了。因为b 首先会适配成[[[6], [9]]],然后会适配成[ [[6,6,6], [9,9,9]], [[6,6,6], [9,9,9]] ], 这样b的shape变成了[2,2,3], 而a还是[2,1,3];那么因为a的第二维度的长度为1, 因此a的维度也会自动适配,适配之后的a变为[ [[1,2,3], [1,2,3]], [[2,3,4], [2,3,4]]]。所以这个时候做a - b运算,实际上,在完成互相适配后,是在做如下数组之间的运算:

a - b = [ [[1,2,3], [1,2,3]], [[2,3,4], [2,3,4]] ] - 
[ [[6,6,6], [9,9,9]], [[6,6,6], [9,9,9]] ]

因此也就得到了上述答案:

>>> a - b
tensor([[[-5, -4, -3],
         [-8, -7, -6]],

        [[-4, -3, -2],
         [-7, -6, -5]]])

但,这不是我们最初想要的答案。我们想要的是torch.tensor([[[1,2,3]], [[2,3,4]]])变成 torch.tensor([ [[1-6, 2- 6, 3 - 6]], [[2-9, 3-9, 4-9]] ])。为了达成我们的效果,我们可以做逆向思维。我们想要的答案需要对如下两个数组运算才能达到:

a - b = [ [[1,2,3]], [[2,3,4]] ] - 
[[[6,6,6]], [[9,9,9]]]

那么,接下来我们对[[[6,6,6]], [[9,9,9]]]做压缩,首先做维度长度的压缩,把这个数组变成[[[6]], [[9]]],这个数组的shape变成了[2,1,1],然后第0个维度的长度已经无法进行压缩了,因为[[6]]和[[9]]不相等。接下来,我们看维度数量能不能压缩,压缩维度数量只能搜索排序在前面而且维度长度为1的维度。这里,第0维度的长度是2,因此维度数量无法再进行压缩。所以最后要达成我们的效果b的最简形式为[[[6]], [[9]]]。接下来,让我们验证一下:

>> a 
tensor([[[1, 2, 3]],

        [[2, 3, 4]]])
>>> b = torch.tensor([[[6]], [[9]]])
>>> a - b
tensor([[[-5, -4, -3]],

        [[-7, -6, -5]]])

这个结果,是符合我们预期的。我们知道 a.sum(-1) = [[6], [9]]的,那我们如何优雅的让a.sum(-1)变成当前b的形态,然后再做a - b的运算呢?答案是,使用unsqueeze函数。

>>> a
tensor([[[1, 2, 3]],

        [[2, 3, 4]]])
>>> a.sum(-1).unsqueeze(-1)
tensor([[[6]],

        [[9]]])
>>> a - a.sum(-1).unsqueeze(-1)
tensor([[[-5, -4, -3]],

        [[-7, -6, -5]]])

unsqueeze函数能够给指定维度位置添加维度。我们可以利用它来改变数组的维度。

通过以上说明和示例,我们应该理解到,本质上不同维度形状的数组是不能做四则运算的。要进行四则运算,则需要自动适配。要自动适配成功,就必须满足一些条件。要想利用自动适配原理,来设计不同维度形状之间的数组做运算,可以采用反推的方法,看看当前的数组需要进行怎样变形或者不变形来达到自己的需求。

pytorch中的损失函数

损失函数是用来计算预测结果与期望结果之间的误差。pytorch作为一个成熟的人工智能框架,提供了很多损失函数供我们使用。

CrossEntropy

这个损失函数是在分类问题上经常用到的。它可以直接被用来比较[[a0,a1,a2],[b0,b1,b2]]和[pa, pb]之间的误差。这两个数组什么意义呢?a0,a1,a2的数字分别代表第0轴上的第0号元素在第1轴上3个分类的权重数值。而pa的取值可能是0,1和2。举一个实际例子说明,在对图像识别分类的问题上,我们给图像的种类用数字来分类。例如,这种图如果是狗,则其编号为0;是猫,则编号为1;是牛则编号为2。那么我们给出一个原始图像数据,然后通过一系列神经网络层后,这幅图片对应的数据变成了[a0,a1,a2],其中a0表示这种图片是狗的可能数值代表为a0, 是猫的可能数值代表为a1, 是牛的可能数值代表为a2。如果我们知道这个图片的实际上是一只猫,那就是说,这个图片的实际对应编号为1。那么我们的CrossEntropy就可以计算[a0,a1,a2]和1之间的差异。

从抽象的宏观角度来看,损失函数本质上给人们提供了一种对比不同意义的数据之间的差异。我们从来都是知道矢量和标量的对比是没有意义的,但是,损失函数就是对这个概率的挑战。

在数学公式上看,这个函数实际上是对比预测数组1中,在期望序号的那个数据与0之间的差异。即如果假设预测数组为[a0,a1,a2],而期望序号为i, 那么这个损失函数计算的值为 0 - log(e^ai/ (e^a0 + e^a1 + e^a2))的大小。如果有很多组数据的话,例如[[a0, a1, a2], [b0, b1, b2]]。那么要衡量多组数据的整体误差,则可以是计算各组误差的平均值,或者计算这些误差的和。

数组变形总结

在人工智能或者很多数据处理的应用中,我们都会遇见各种数组变形的问题。本文将会对pytorch和numpy中各种数组变形函数做个列表总结。

torch.unsqueeze

unsqueeze可以给数组增加一个维度,并指定维度增加的位置。例如如果有一个数组[1,2,3],我们希望给这个数组的0维位置增加一个维度变成[[1,2,3]],则可以

>>> torch.unsqueeze(torch.tensor([1,2,3]), 0)
tensor([[1, 2, 3]])

如果想给这个数组的最后一个维度,或者说,第1个维度增加维度变成[[1],[2],[3]]。则可以采取如下方案:

>>> torch.unsqueeze(torch.tensor([1,2,3]), -1)
tensor([[1],
        [2],
        [3]])
>>> torch.unsqueeze(torch.tensor([1,2,3]), 1)
tensor([[1],
        [2],
        [3]])

torch.flatten, numpy.flatten

这两个函数的作用一样,就是把多维度的数组变成一个维度的数组。

例如把[[1,2,3], [4,5,6]]变成[1,2,3,4,5,6]

torch.nn.flatten

这个flatten函数与前面的不同之处不仅仅在与它的定位是网络层。这里的flatten还可以指定flatten的维度的位置。例如把[[[1,2,3], [4,5,6], [7,8,9]]]转化为[1,2,3,4,5,6,7,8,9]或者[[1,2,3], [4,5,6], [7,8,9]]

>> a = torch.tensor([[[1,2,3], [4,5,6], [7,8,9]]])
>>> m = torch.nn.Flatten(0, -1)
>>> m(a)
tensor([1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> m = torch.nn.Flatten(0, 1)
>>> m(a)
tensor([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]])

安装cuda + pytorch 的注意事项

  1. 需要确定电脑上的显卡是否支持cuda,当前的显卡是支持哪个cuda版本。在linux版本上,需要给显卡安装nvidia驱动,然后运行nvidia-smi指令查看显卡驱动信息。其结果可能如下所示
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 430.50       Driver Version: 430.50       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GT 730      Off  | 00000000:01:00.0 N/A |                  N/A |
| 52%   43C    P0    N/A /  N/A |    695MiB /   974MiB |     N/A      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    0                    Not Supported                                       |
+-----------------------------------------------------------------------------+

这样的信息就表示当前显卡驱动支持的是cuda 10.1版本

2. 在安装pytorch的时候,需要安装与显卡当前同样cuda版本的软件包。在pytorch上提供的可以安装cuda版本比较多,一般首页提示上只有最新的几个版本。如果这个没有电脑上显卡nvidia-smi指令看到的版本,则需要去pytorch的历史版本上去找。https://pytorch.org/get-started/previous-versions/

n维矩阵的转置

首先我们需要确定矩阵转置更广泛的定义,n维矩阵具有n个坐标轴。我们假设一个n维矩阵的n个坐标轴依次是a0, a1, a2, a3, ... an-1。对此矩阵的转置可以看作是构建一个新的n维矩阵,但是,要把原先坐标轴的顺序改变一下。这个改变可以是任意的,因此,在定义n维矩阵的转置时必须要定义一下坐标轴的排列顺序。在numpy中,如果没有定义此顺序,则默认的新顺序是原来坐标轴顺序的相反序列,即: an-1, an-2, ... a3, a2, a1。

如果,我们假设转置后的新的坐标轴顺序为at0, at1, at2, ...atn-1。并假设原矩阵为m, 转置后的矩阵为mt。那么,对于新的矩阵中的每一个元素,mt(i0,i1,i2, ...in-1) = m(j0, j1, j2, ..., jn-1), 其中, j0 = iat0, j1 = iat1, ... jn-1 = iatn-1。

简单的举个3维矩阵的例子:

>>> a = np.arange(24).reshape(2,3,4)
>>> a
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]],

       [[12, 13, 14, 15],
        [16, 17, 18, 19],
        [20, 21, 22, 23]]])

示例中矩阵a是一个3维矩阵,其中轴0有2个元素,轴1有3个元素,轴2有4个元素。如果我们对此矩阵做轴(0, 2, 1)的转置,则对于新的矩阵而言,存在如下的映射:

mt(0,0,0) = m(0,0,0) = 0
mt(0,0,1) = m(0,1,0) = 4
mt(0,0,2) = m(0,2,0) = 8

mt(0,1,0) = m(0,0,1) = 1
mt(0,1,1) = m(0,1,1) = 5
mt(0,1,2) = m(0,2,1) = 9

mt(0,2,0) = m(0,0,2) = 2
mt(0,2,1) = m(0,1,2) = 6
mt(0,2,2) = m(0,2,2) = 10

mt(0,3,0) = m(0,0,3) = 3
mt(0,3,1) = m(0,1,3) = 7
mt(0,3,2) = m(0,2,3) = 11

mt(1,0,0) = m(1,0,0) = 12
mt(1,0,1) = m(1,1,0) = 16
mt(1,0,2) = m(1,2,0) = 20

mt(1,1,0) = m(1,0,1) = 13
mt(1,1,1) = m(1,1,1) = 17
mt(1,1,2) = m(1,2,1) = 21

mt(1,2,0) = m(1,0,2) = 14
mt(1,2,1) = m(1,1,2) = 18
mt(1,2,2) = m(1,2,2) = 22

mt(1,3,0) = m(1,0,3) = 15
mt(1,3,1) = m(1,1,3) = 19
mt(1,3,2) = m(1,2,3) = 23

通过代码检验以上结果:

>>> a.transpose([0,2,1])
array([[[ 0,  4,  8],
        [ 1,  5,  9],
        [ 2,  6, 10],
        [ 3,  7, 11]],

       [[12, 16, 20],
        [13, 17, 21],
        [14, 18, 22],
        [15, 19, 23]]])

当我们因为应用需求,需要改变矩阵中坐标轴的排列顺序时,则需要使用转置操作。

创建python包并发布到指定仓库

1创建如下文件结构

packaging_tutorial
├── LICENSE
├── README.md
├── example_pkg
│ └── init.py
├── setup.py
└── tests

2 配置setup.py

import setuptools

with open("README.md", "r") as fh:
long_description = fh.read()

setuptools.setup(
name="example-pkg-YOUR-USERNAME-HERE", # Replace with your own username
version="0.0.1",
author="Example Author",
author_email="author@example.com",
description="A small example package",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/pypa/sampleproject",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires='>=3.6',
)

3 准备README.md

这一步其实是选择性的,这依赖于第2步时怎么去读取long_description

4 创建安装包

python3 setup.py sdist bdist_wheel

这一步选择性的需要更新一下setuptools和wheel

python3 -m pip install --user --upgrade setuptools wheel

5 安装twine

python3 -m pip install --user --upgrade twine

6 发布安装包到指定仓库

python3 -m twine upload --repository-url http://npm.tuocad.com/repository/pypl-hosted/ dist/*

7 安装下载使用此安装包

pip install -i http://npm.tuocad.com/repository/pypl/simple --no-deps geometryfeaturerecognize==0.0.1 --trusted-host npm.tuocad.com

或者指定python3

python3 -m pip install -i http://npm.tuocad.com/repository/pypl/simple --no-deps geometryfeaturerecognize==0.0.1 --trusted-host npm.tuocad.com

要特别注意这里url中的simple

python的内存管理机制

智能指针这个东西,基本上是各种系统内存管理的一个标配。所有的python对象都继承自PyObject对象,每当该对象多一个引用,其内置计数器就+1。每少一个就-1。当其减少到0时,就自动释放该内存。

但仅仅是智能指针是不够的,因为它解决不了循环引用的问题。python还采用了gc模块来解决循环引用的问题。gc模块的具体机制可以参考下文:

https://rushter.com/blog/python-garbage-collector/

总的来说,一般情况下我们是不用担心python内存释放的问题。如果你想手动做更多优化,可以考虑调用gc模块的手动释放内存的函数

In some cases, it is useful to disable GC and use it manually. The automatic collection can be disabled by calling gc.disable(). To manually run the collection process, you need to use gc.collect().

字符的解码与编码

了解和学习字符的解码与编码技术是破解各种乱码问题的关键。字符的解码与编码其实时时刻刻都发生在我们用电脑的过程中。当你打开一个文本文件的时候,当你打开一个网页的时候,当你调用python的request库,访问response.text的时候解码流程就发生了。相对应的,当你保存一个文件的时候,当你发送一段网络请求的时候,编码流程就发生了。那么,解码和编码的时候到底发生了什么?

字符的编码

在计算机中,所有的字符都是2进制的数字。每一个字符,包括你看到本文的每一个字在都是以0101010110这样的形式保存在计算机中。当我们打开一个文件准备看那个字符的时候,计算机才会拿出一个“字典”来,去看相应的2进制数字对应的是哪个“图”,然后再把那个“图”显示在屏幕上,也就成了我们眼中的字。这个查“图”的过程并不是字符解码的过程,本文所讨论的问题的矛盾是世界上所用的字符太多,而通常一个人所使用的字符又非常有限。那么计算机到底是应该更多的考虑字符解析的全面性,还是更多的考虑到个人习惯而减少占用内存的问题。

比如说,一个人只使用汉字和英文,那么它只需要考虑英文的26个字符大小写和相应标点符号以及所有汉字就够了。这可能只需要2个字节就足够对应他能用到的所有字符。而世界上所有的字符需要4个字节来表达(目前公认的有110多万个字符)。那么当我们保存一个文本后,其他人在完全不知道此文本内容的前提下,要想完全保证能正常打开此文本,一般则需要此文本顾全那110多万的字符,每个字符用4个字节编码。但其实如果此人知道此文件只有英文和汉字,那么每个字符只需要2个字节编码就够了。这里的内存量相差一倍。这一倍的大小,在电脑存储中或许不算什么,但是如果放在网络带宽传输中,可就大了。因此这里就引入了字符编码的概念,我们可以用少量的字节编码文本,而不考虑其他的一般在这个区域内用不到的字符,就可以节省空间和带宽。

但其实问题依然存在,因为你永远也不能保证你的文件会不会有一天流传到另外一个遥远的国家去。例如,你从中国去美国出差,然后收到来自中国的文件,等你打开一看,可能全是乱码。要想正常的看到这个文件,你还需要转码技术。后文我们再提转码技术,现在我们考虑有没有一种可伸缩的编码技术,可以兼顾全面性和内存占用量。其实,是有的,例如utf-8就是这样诞生的。utf-8规定当一个字节小于128时,则按照一个字节保存一个字符的方式来解读。当一个字节大于或等于128时,再考虑根据它的下一位字节来一起联合解读。也就是说,utf-8编码中,每个字节的8位里,用最高位用来控制字节的连续性。这样,在utf-8文件里,就有可能是一个字节表示一个字符,也可能2,3,4个字节表示一个字符。它能根据需求,自动的伸缩字节位。我们只要把世界上相对常用的字符用更少的位数对应就能比较好的兼顾全面性和内存占用量。类似的,还有utf-16, utf-32等编码。目前,比较全球通用,程序员的最爱编码就是utf-8。而中国国内的微软系统,比较通用的是gbk(国标扩)编码(相对于汉字内存占用更优)。

字符的解码

所谓解码,其实是将一种编码转化为另外一种编码,这个“另外一种编码“是计算机更容易理解而且适用性更全面的编码——unicode。也可以这么说,所谓字符的解码,就是将其他的编码方式转化成unicode的编码方式。unicode用了110多万个指针,指向了目前世界上在比较大范围内使用的所有字符。这些字符包括了英语,日语,中文,韩文,阿拉伯文,俄罗斯文,法文,外星文。。。因此,无论你是哪个国家的,采用了什么编码,只有定义了与unicode间转化的方法,那么我就能知道你那个编码指的是哪个字符。因此,我们打开一个文本文件的时候,都发生了一次由文件编码转化为unicode的过程。

每一台电脑都会根据自己的配置来对文件进行解码。例如中文的微软系统,会按照默认的gbk方式来对文本进行解码。fedora会按照utf-8的方式来对文本进行解码。有些更只能一些的程序,还会对文本文件进行多次解码的尝试。例如vim可以通过:set fileencodings来配置几个按照优先级排列的候选解码方式,当一个解码失败,则用另一个解码方式替代。更进一步的,还有一些更智能的探测编码方式的程序,能探测一个文件的编码,再用相应的解码方式来解码。例如Python的chardet模块和本公司对step文件优化的StepEncodeDetector模块等。

这里还需要提到一点的就是,python3与python2的区别之一,就是python3的所有str类型都采用了unicode的编码。

字符的转码

字符的转码一般根据发生时间有两种情况,一种是在知道了某个文件的编码形式,再直接对该文件按照某编码格式进行解码,再按照另外一种格式进行编码。实现此方案的python模块有codec(codec.open(filename, encode='encode'))。另外一个情况,是在打开某文件前不知道其编码形式,在错误的解码完成后还需要对其转码的情况。这种情况就稍微复杂一些了,我们需要想其他方法,例如问清除此文件的来源或者智能的试探的方式来确定其编码形式。在确定了之后,我们需要对已经错误解码的字符进行编码还原后,再做正确的解码。例如,某个string已经被按照latin1错误的解码了,但我们知道了其原来的编码是gbk,这时候我们可以做如下操作:

string.encode('latin1').decode('gbk')

python中用4元数实现3D旋转

利用4元数实现3d旋转有非常多的优点,相比于4x4矩阵,欧拉角的方式具有更好的计算高效性。一般的,4元数(x,y,z,w)表达空间旋转的表达方式如下:

设空间单位向量(a,b,c), 和一个夹角theta。则表示绕空间向量(a,b,c)沿顺时针(沿该向量相同方向看)旋转theta角的四元数为q = (a*sin(theta/2), b*sin(theta/2), c*sin(theta/2), cos(theta/2))。

若,空间上某向量v,按照四元数q旋转后得到的值为u,则

u = qvq(-1) (这里的乘法方式为哈密尔顿积

其中q(-1)指的是四元数q的逆,对于单位4元数的逆,恰恰等于原四元数的虚数部分去反。即,

若q = xi + yj + zk + w, 则q(-1) = -xi -yj -zk + w

除此以外,空间的多个旋转还可以串连起来,例如:

q = q2q1 计算所的的q为先按照q1旋转,再按照q2旋转的4元数。

实际工程应用中,可以使用numpy的numpy-quaternion和scipy的scipy.spatial.transform.Rotation来配合实现4元数的运算,以及4元数与3x3或4x4矩阵的转化。

示例代码如下:(如下代码要求scipy >= 1.4.0)

from scipy.spatial.transform import Rotation as R
import numpy as np
def makeRotationMatrix(theta, axis):
     m4 = np.identity(4)
     s = np.sin(theta/2)
     c = np.cos(theta/2)
     quat = np.zeros([4])
     quat[0:3] = axis * s
     quat[3] = c
     r = R.from_quat(quat)
     m3 = r.as_matrix()
     m4[0:3, 0:3] = m3
     return m4

def makeTranslationMatrix(vec):
     m4 = np.identity(4)
     m4[0:3,3] = vec
     return m4