介绍
今天开始一个新的系列,这个系列的目标是用python在不使用任何第三方库的情况下去实现各类机器学习或者深度学习的算法。之所以会有这种想法是因为每当我想提高编程技巧的时候,我总希望能够做一些简单又有趣的小项目练手。我一直对机器学习算法颇感兴趣,所以我想为什么不用python从零开始搭建一套迷你机器学习库呢。于是我尝试这么做了,这个系列就是记录我实现这一想法的过程。另外,由于这个项目很少用到第三方库,而且实现上尽可能抱着语法简单,因此也较为容易转换成其他语言。
说起来,在没有开始这个项目之前,有些东西,比如Numpy里的array赋值、取值、转置这些,用起来跟呼吸一样自然,并造成一种...就像被问1+1为什么等于2的感觉:它就是应该等于2没有为什么。但真正深入实现的时候,发现又不是这么一回事...
本篇是系列的第一篇,主要是模仿numpy的部分功能,搭建一个矩阵计算框架。当然,这个实现不会像商业库那样拥有强大的功能以及稳定性,因而会有些不那么robust。但对于抱着学习的目的来说,忽略一些复杂情况可以更容易理解本质。我打打算是每一期的代码都是最简实现,够用就行,只有后面实现算法时需要用到新功能时才会新增功能。那么下面就开始这个矩阵计算框架的第一步,Vector类。
1.Vector类
矩阵运算首先得要有矩阵,numpy里面矩阵的展现形式是ndarray这个类,pytorch或者tensorflow都叫Tensor。我这里起个名字叫Vector吧,用于存储矩阵的数据结构。
1.1 初始化函数
关于Vector类,有两个必不可少的类成员属性,一是用于存储数值的变量array,二是用于表示矩阵形状的变量shape。
总之,我们希望如果对Vector进行索引的话,得到的东西还是个vector。所以最简单的想法就是,array里装的是低一维的Vector。这样索引的时候直接可以对array索引直接取到响应的Vector了。这样,array在初始化的时候需要做一些额外的操作,直接看代码好了:
class Vector:
def __init__(self, data, shape=None, requires_grad=False, grad=None, _creator=None, name='Unknown'):
self.name = name
if not shape:
self.shape = _inference_shape(data)
else:
self.shape = deepcopy(shape)
if self.ndim > 1:
self.array = [v if isinstance(v, Vector) else Vector(v, shape=self.shape[1:]) for v in data]
else:
self.array = [cast(v) for v in data]
self.grad = grad
self.requires_grad = requires_grad
self._creator = _creator
上述代码看到,初始化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。这么做的原因说主要是想把value的传递方式从按值传递转变为引用传递。考虑到当vector进行转置操作时会新生成一个vector,但是我们希望对转置后的vector进行修改时,原始vector的值也跟着修改,毕竟他俩是同一个东西,只不过shape变了而已。如果使用原始数据类型的话,赋值时会按值传递的,也就无法实现这一效果。
题外话:关于变量array存储数据的形式,我在这里还踩了两个坑。一开始我很天真,以为多维矩阵就是list的嵌套,即Vector的array就是诸如:[[1,2,3],[4,5,6]]。但是随着进度继续,我发现单纯list的嵌套会有很多麻烦的地方。其中第一不和谐的点是,如果array为list的嵌套,对Vector的某个维度进行索引的时候,取出来是个list,而不是一个Vector。尽管在复写__getitem__的时候,可以构造一个新的Vector实例,但每次索引都要初始化一个Vector,这会影响索引时的速度...当然ÿ