python list 按照指定的index重排顺序_从零开始搭建机器学习算法框架(python)--计算框架...
介绍本篇主要是模仿numpy的部分功能,搭建一个数值计算框架。目的是实现机器学习算法中涉及到基本的矩阵运算或操作。1.Vector类矩阵运算首先得要有矩阵,numpy里面矩阵的展现形式是ndarray这个类,pytorch或者tensorflow都叫Tensor。我这里起个名字叫Vector吧,用于存储矩阵的数据结构。1 初始化函数关于Vector类,有两个必不可少的类成员属性,一是用于存储数值的
介绍
本篇主要是模仿numpy的部分功能,搭建一个数值计算框架。目的是实现机器学习算法中涉及到基本的矩阵运算或操作。
1.Vector类
矩阵运算首先得要有矩阵,numpy里面矩阵的展现形式是ndarray这个类,pytorch或者tensorflow都叫Tensor。我这里起个名字叫Vector吧,用于存储矩阵的数据结构。
- 1 初始化函数
关于Vector类,有两个必不可少的类成员属性,一是用于存储数值的变量array,二是用于表示矩阵形状的变量shape。
题外话:关于变量array存储数据的形式,我在这里还踩了两个坑。一开始我很天真,以为多维矩阵就是list的嵌套,但是随着进度继续,我发现单纯list的嵌套会有很多麻烦的地方。其中第一不和谐的点是,如果array为list的嵌套,对Vector的某个维度进行索引的时候,取出来是个list,而不是一个Vector。尽管可以在复写__getitem__的时候,构造一个新的Vector实例,但这会影响索引时的速度...另外一点是,我发现一旦维度大于2,list嵌套实在太难写转置操作了。于是我打算尽可能维持这个vector取数据的过程类似线性表(当然python里没有严格的线性表,list是线性表和链表的结合)的特性,二是希望这个存储结构有利于转置操作。我的第二个想法是,array的存储结构就是一个一维的list。对vector取值时,可以根据shape对index进行映射到array相应的index上,来达到索引指定位置的目的,思路类似下图(图一)。据说numpy的C实现的底层就是这么设计的,不过这种设计转置写起来感觉更麻烦了。于是放弃了这个方案。
总之,我们希望如果对Vector进行索引的话,得到的东西还是个vector。所以最简单的想法就是,array里装的是低一维的Vector。这样索引的时候直接可以对array索引直接取到响应的Vector了。这样,array在初始化的时候需要做一些额外的操作,直接看代码好了:
class Vector:
def __init__(self, data, shape=None, requires_grad=True, _creator=None, grad=None, name='Unknown'):
if not shape:
shape = _inference_shape(data)
else:
shape = deepcopy(shape)
if len(shape) > 1:
self.array = [elem if isinstance(elem, Vector) else Vector(elem, shape=shape[1:]) for elem in data]
else:
_array = []
for elem in data:
if isinstance(elem, int):
elem = Int(elem)
elif isinstance(elem, float):
elem = Float(elem)
elif isinstance(elem, BaseType):
pass
_array.append(elem)
self.array = _array
self.shape = shape
self._creator = _creator
self.requires_grad = requires_grad
self.grad = grad
self.name = name
上述代码看到,初始化Vector最主要是两个参数,一个是data即存了什么样的数据,另一个是shape,表明了数据按照什么样的格式存储的(其他参数是自动求导所需要的,后一期再做介绍)。需要注意的是,如果data本身具备多维的结构,比如嵌套的list,那么shape可以为None。此时,shape可以从data的嵌套结构中推测出来,即_inference_shape()这个函数。该函数的作用是给定一个多维list,并返回这个list的shape,具体实现可在源码中寻找。
接下来,如果data是一个高维的结构,那么array里存的是低一维度的Vector,因此可以通过递归构造Vector。当data是一维的时候,意味着array里存的是数值,直接把data存入array就好了。这里需要注意的是我为python中的int以及float分别新建了一个类Int以及Float。这么做的原因会在后文提及。
1.2 数据索引取值
python中如果想实现对某个类的实例进行索引取值,就是重写__getitem__方法。类似numpy,索引支持切片,即vector[1:3],以及多维度索引,vector[1,2,3]。
def __getitem__(self, index):
if isinstance(index, int):
return self.array[index]
elif isinstance(index, slice):
start = index.start if index.start else 0
end = index.stop if index.stop else self.shape[0]
step = index.step if index.step else 1
res_array = [self[i].array if isinstance(self[i], Vector) else self[i] for i in range(start, end, step)]
elif isinstance(index, tuple) or isinstance(index, list):
if len(index) < self.ndim:
index = list(index)
index.extend([slice(None, None, None)] * (self.ndim - len(index)))
def recursive_getitem(vector, index_):
if len(index_) > 0:
res_array_ = []
if isinstance(index_[0], slice):
for elem in vector.array[index_[0]]:
res_array_.append(recursive_getitem(elem, index_[1:]))
else:
res_array_ = recursive_getitem(vector.array[index_[0]], index_[1:])
else:
return vector
return res_array_
res_array = recursive_getitem(self, index)
else:
raise Exception()
if isinstance(res_array, list):
return Vector(res_array)
else:
return res_array
__getitem__有一个入参index,表示索引的位置。由于既要支持普通索引,又要支持切片操作同时还要支持高维索引,所以index的类型有三种情况,第一个是最普通的,传入一个int型数值,第二种传进来的是slice。前两种都是在单一维度上的索引。第三种情况传进来的是一个元祖(或list),表示在多个维度上的索引。最后一个情况处理起来稍微麻烦些,不过思路却很简单:由于需要在多个维度上索引,很容易想到按照顺序每次只处理一个维度,这样高维索引问题可以转化为多次一维的索引。用递归很容易解决,只是需要留意下高维索引和切片索引结合的情况,即vector[1,:,3]。
1.3 索引赋值
索引赋值在python中就是重写__setitem__方法。实际上一旦索引取值完成了,赋值就是水到渠成的事情。
def __setitem__(self, key, value):
if isinstance(key, int):
if isinstance(value, BaseType):
self.array[key] = value
elif isinstance(value, int):
self.array[key] = Int(value)
elif isinstance(value, float):
self.array[key] = Float(value)
else:
self.array[key] = value
elif isinstance(key, list) or isinstance(key, tuple):
vector = self
for k in key[:-1]:
vector = vector[k]
if isinstance(value, BaseType):
vector[key[-1]] = value
elif isinstance(value, int):
vector[key[-1]] = Int(value)
elif isinstance(value, float):
vector[key[-1]] = Float(value)
else:
vector[key[-1]] = value
与取值类似,key(对应于__getitem__里的index,原谅我命名没有统一...)有三种情况(上面代码漏写了一种情况)。由于索引取值已经在__getitem__里实现了,剩下的只是赋值而已。这里也只是提醒一点,如果value是python内置的数据类型需要转化成自定义的Int或Float。说下为什么要这么做,这里主要是把value的传递方式从按值传递转变为引用传递。一方面是考虑到这种方式可以节约一些内存,更重要的是在一些情况下这是必须的,比如转置操作会新生成一个vector,但是我们希望对转置后的vector进行修改时,原始vector的值也跟着修改,毕竟他俩是同一个东西,只不过shape变了而已。
1.4 转置与交换维度
接下来是一个比较重要的功能,转置。说实话,可能是我比较反应比较迟钝,转置这个操作卡了我很长时间。因为我始终觉得转置其实就是几个轴交换顺序了,所以我一直想的是如何给Vector加上轴的概念(类似于图一的结构)这样转置会变得异常简单。但真正做下去以后,总觉得加轴反而会把Vector的结构弄复杂了,于是放弃了这个思路。我现在的实现方式如下:
def transpose(self, axises=None):
new_shape = [self.shape[axises[i]] for i in range(self.ndim)]
new_vec = zeros(new_shape)
curr_index = [0 for _ in range(self.ndim)]
target_idx = [0 for _ in range(new_vec.ndim)]
while 1:
for axis in range(self.ndim - 1):
if curr_index[axis] == self.shape[axis]:
curr_index[axis] = 0
curr_index[axis + 1] += 1
if curr_index[-1] >= self.shape[-1]:
break
for i, j in enumerate(axises):
target_idx[i] = curr_index[j]
new_vec[target_idx] = self[curr_index]
curr_index[0] += 1
return new_vec
transpose接收一个参数axises,表示维度的交换顺序:原始的维度为[0,1,2,3],如果打算交换第0维与第2维的位置,则axises为[2,1,0,3]。我这里的实现思路可能不够高效,但好在还算简单直白,首先用转置后的shape初始化一个新的vector,然后遍历原始vector,把数值一个个塞到新的vector里去。遍历的过程比较原始,就是通过构造多维索引curr_index,从最低位开始遍历,每次循环在最低位+1,不过需要注意“进位”。另外target_idx 是转置后对curr_index的映射。
2.若干工具函数
_inference_shape:功能是推断嵌套list的shape。
def _inference_shape(data):
shape = []
while isinstance(data, list):
shape.append(len(data))
data = data[0]
return shape
create_array_by_shape:与_inference_shape功能相反,是通过shape构造嵌套的list。
def create_array_by_shape(shape, val):
if isinstance(shape, int):
return val
else:
new_shape = shape[1:] if len(shape) > 1 else shape[0]
return [create_array_by_shape(new_shape, val) for _ in range(shape[0])]
zeros和ones:构造一个0或1组成的Vector实例。
def zeros(shape, _creator=None):
array = create_array_by_shape(shape, 0)
return Vector(array, shape=shape, _creator=_creator)
def ones(shape, _creator=None):
array = create_array_by_shape(shape, 1)
return Vector(array, shape=shape, _creator=_creator)
结语
至此,一个简单Vector类别就构造好了,作为一个矩阵计算框架,还需要实现各种运算方法,由于我希望把自动求导的功能集成到Vector里,所以运算这部分的内容实现起来会稍微麻烦一些,留到下期再说。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)