引言

本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。

要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不适用外部完备的框架前提下,实现我们想要的模型。本系列文章的宗旨就是通过这样的过程,让大家切实掌握深度学习底层实现,而不是仅做一个调包侠。
本系列文章首发于微信公众号:JavaNLP

在上篇文章中,我们实现了反向传播的模式代码。同时正确地实现了加法运算和乘法运算。从今天开始,我们就来实现剩下的运算。本文实现了减法、除法、矩阵乘法和求和等运算。

实现减法运算

我们先编写测试用例,再实现减法计算图。

test_tensor_sub.py:

import numpy as npfrom core.tensor import Tensordef test_simple_sub():x = Tensor(1, requires_grad=True)y = Tensor(2, requires_grad=True)z = x - yz.backward()assert x.grad.data == 1.0assert y.grad.data == -1.0def test_array_sub():x = Tensor([1, 2, 3], requires_grad=True)y = Tensor([4, 5, 6], requires_grad=True)z = x - yassert z.data.tolist() == [-3., -3., -3.]z.backward(Tensor([1, 1, 1]))assert x.grad.data.tolist() == [1, 1, 1]assert y.grad.data.tolist() == [-1, -1, -1]x -= 0.1assert x.grad is Nonenp.testing.assert_array_almost_equal(x.data, [0.9, 1.9, 2.9])def test_broadcast_sub():x = Tensor([[1, 2, 3], [4, 5, 6]], requires_grad=True)  # (2, 3)y = Tensor([7, 8, 9], requires_grad=True)  # (3, )z = x - y  # shape (2, 3)assert z.data.tolist() == [[-6, -6, -6], [-3, -3, -3]]z.backward(Tensor(np.ones_like(x.data)))assert x.grad.data.tolist() == [[1, 1, 1], [1, 1, 1]]assert y.grad.data.tolist() == [-2, -2, -2]

然后实现减法的计算图。

class Sub(_Function):def forward(ctx, x: np.ndarray, y: np.ndarray) -> np.ndarray:'''实现 z = x - y'''ctx.save_for_backward(x.shape, y.shape)return x - ydef backward(ctx, grad: Any) -> Any:shape_x, shape_y = ctx.saved_tensorsreturn unbroadcast(grad, shape_x), unbroadcast(-grad, shape_y)

这些类都添加到ops.py中。然后跑一下测试用例,结果为:

============================= test session starts ==============================
collecting ... collected 3 itemstest_sub.py::test_simple_sub PASSED                                      [ 33%]<class 'numpy.ndarray'>test_sub.py::test_array_sub PASSED                                       [ 66%]<class 'numpy.ndarray'>test_sub.py::test_broadcast_sub PASSED                                   [100%]<class 'numpy.ndarray'>============================== 3 passed in 0.36s ===============================

实现除法运算

编写测试用例:

import numpy as npfrom core.tensor import Tensordef test_simple_div():'''测试简单的除法'''x = Tensor(1, requires_grad=True)y = Tensor(2, requires_grad=True)z = x / yz.backward()assert x.grad.data == 0.5assert y.grad.data == -0.25def test_array_div():x = Tensor([1, 2, 3], requires_grad=True)y = Tensor([2, 4, 6], requires_grad=True)z = x / yassert z.data.tolist() == [0.5, 0.5, 0.5]assert x.data.tolist() == [1, 2, 3]z.backward(Tensor([1, 1, 1]))np.testing.assert_array_almost_equal(x.grad.data, [0.5, 0.25, 1 / 6])np.testing.assert_array_almost_equal(y.grad.data, [-0.25, -1 / 8, -1 / 12])x /= 0.1assert x.grad is Noneassert x.data.tolist() == [10, 20, 30]def test_broadcast_div():x = Tensor([[1, 1, 1], [2, 2, 2]], requires_grad=True)  # (2, 3)y = Tensor([4, 4, 4], requires_grad=True)  # (3, )z = x / y  # (2,3) * (3,) => (2,3) * (2,3) -> (2,3)assert z.data.tolist() == [[0.25, 0.25, 0.25], [0.5, 0.5, 0.5]]z.backward(Tensor([[1, 1, 1, ], [1, 1, 1]]))assert x.grad.data.tolist() == [[1/4, 1/4, 1/4], [1/4, 1/4, 1/4]]assert y.grad.data.tolist() == [-3/16, -3/16, -3/16]

# Python3 只有 __truediv__ 相关魔法方法
class TrueDiv(_Function):def forward(ctx, x: ndarray, y: ndarray) -> ndarray:'''实现 z = x / y'''ctx.save_for_backward(x, y)return x / ydef backward(ctx, grad: ndarray) -> Tuple[ndarray, ndarray]:x, y = ctx.saved_tensorsreturn unbroadcast(grad / y, x.shape), unbroadcast(grad * (-x / y ** 2), y.shape)

由于Python3只有 __truediv__ 相关魔法方法,因为为了简单,也将我们的除法命名为TrueDiv

同时修改tensor中的register方法。

至此,加减乘除都实现好了。下面我们来实现矩阵乘法。

实现矩阵乘法

先写测试用例:

import numpy as np
import torchfrom core.tensor import Tensor
from torch import tensordef test_simple_matmul():x = Tensor([[1, 2], [3, 4], [5, 6]], requires_grad=True)  # (3,2)y = Tensor([[2], [3]], requires_grad=True)  # (2, 1)z = x @ y  # (3,2) @ (2, 1) -> (3,1)assert z.data.tolist() == [[8], [18], [28]]grad = Tensor(np.ones_like(z.data))z.backward(grad)np.testing.assert_array_equal(x.grad.data, grad.data @ y.data.T)np.testing.assert_array_equal(y.grad.data, x.data.T @ grad.data)def test_broadcast_matmul():x = Tensor(np.arange(2 * 2 * 4).reshape((2, 2, 4)), requires_grad=True)  # (2, 2, 4)y = Tensor(np.arange(2 * 4).reshape((4, 2)), requires_grad=True)  # (4, 2)z = x @ y  # (2,2,4) @ (4,2) -> (2,2,4) @ (1,4,2) => (2,2,4) @ (2,4,2)  -> (2,2,2)assert z.shape == (2, 2, 2)# 引入torch.tensor进行测试tx = tensor(x.data, dtype=torch.float, requires_grad=True)ty = tensor(y.data, dtype=torch.float, requires_grad=True)tz = tx @ tyassert z.data.tolist() == tz.data.tolist()grad = np.ones_like(z.data)z.backward(Tensor(grad))tz.backward(tensor(grad))# 和老大哥 pytorch保持一致就行了assert np.allclose(x.grad.data, tx.grad.numpy())assert np.allclose(y.grad.data, ty.grad.numpy())

这里矩阵乘法有点复杂,不过都在理解广播和常见的乘法中分析过了。同时我们引入了torch仅用作测试。

在常见运算的计算图中对句子乘法的反向传播进行了分析。我们下面就来实现:

class Matmul(_Function):def forward(ctx, x: ndarray, y: ndarray) -> ndarray:'''z = x @ y'''assert x.ndim > 1 and y.ndim > 1, f"the dim number of x or y must >=2, actual x:{x.ndim}  and y:{y.ndim}"ctx.save_for_backward(x, y)return x @ ydef backward(ctx, grad: ndarray) -> Tuple[ndarray, ndarray]:x, y = ctx.saved_tensorsreturn unbroadcast(grad @ y.swapaxes(-2, -1), x.shape), unbroadcast(x.swapaxes(-2, -1) @ grad, y.shape)

为了适应 (2,2,4) @ (4,2) -> (2,2,4) @ (1,4,2) => (2,2,4) @ (2,4,2) -> (2,2,2)的情况,通过swapaxes交换最后两个维度的轴,而不是简单的转置T

下面来实现聚合运算,像SumMax这些。

实现求和运算

先看测试用例:

import numpy as npfrom core.tensor import Tensordef test_simple_sum():x = Tensor([1, 2, 3], requires_grad=True)y = x.sum()assert y.data == 6y.backward()assert x.grad.data.tolist() == [1, 1, 1]def test_sum_with_grad():x = Tensor([1, 2, 3], requires_grad=True)y = x.sum()y.backward(Tensor(3))assert x.grad.data.tolist() == [3, 3, 3]def test_matrix_sum():x = Tensor([[1, 2, 3], [4, 5, 6]], requires_grad=True)  # (2,3)y = x.sum()assert y.data == 21y.backward()assert x.grad.data.tolist() == np.ones_like(x.data).tolist()def test_matrix_with_axis():x = Tensor([[1, 2, 3], [4, 5, 6]], requires_grad=True)  # (2,3)y = x.sum(axis=0)  # keepdims = Falseassert y.shape == (3,)assert y.data.tolist() == [5, 7, 9]y.backward([1, 1, 1])assert x.grad.data.tolist() == [[1, 1, 1], [1, 1, 1]]def test_matrix_with_keepdims():x = Tensor([[1, 2, 3], [4, 5, 6]], requires_grad=True)  # (2,3)y = x.sum(axis=0, keepdims=True)  # keepdims = Trueassert y.shape == (1, 3)assert y.data.tolist() == [[5, 7, 9]]y.backward([1, 1, 1])assert x.grad.data.tolist() == [[1, 1, 1], [1, 1, 1]]

class Sum(_Function):def forward(ctx, x: ndarray, axis=None, keepdims=False) -> ndarray:ctx.save_for_backward(x.shape)return x.sum(axis, keepdims=keepdims)def backward(ctx, grad: ndarray) -> ndarray:x_shape, = ctx.saved_tensors# 将梯度广播成input_shape形状,梯度的维度要和输入的维度一致return np.broadcast_to(grad, x_shape)

我们这里支持keepdims参数。

下面实现一元操作,比较简单,根据计算图可以直接写出来。

实现Log运算

测试用例:

import mathimport numpy as npfrom core.tensor import Tensordef test_simple_log():x = Tensor(10, requires_grad=True)z = x.log()np.testing.assert_array_almost_equal(z.data, math.log(10))z.backward()np.testing.assert_array_almost_equal(x.grad.data.tolist(), 0.1)def test_array_log():x = Tensor([1, 2, 3], requires_grad=True)z = x.log()np.testing.assert_array_almost_equal(z.data, np.log([1, 2, 3]))z.backward([1, 1, 1])np.testing.assert_array_almost_equal(x.grad.data.tolist(), [1, 0.5, 1 / 3])

class Log(_Function):def forward(ctx, x: ndarray) -> ndarray:ctx.save_for_backward(x)# log = lnreturn np.log(x)def backward(ctx, grad: ndarray) -> ndarray:x, = ctx.saved_tensorsreturn grad / x

实现Exp运算

测试用例:

import numpy as npfrom core.tensor import Tensordef test_simple_exp():x = Tensor(2, requires_grad=True)z = x.exp()  # e^2np.testing.assert_array_almost_equal(z.data, np.exp(2))z.backward()np.testing.assert_array_almost_equal(x.grad.data, np.exp(2))def test_array_exp():x = Tensor([1, 2, 3], requires_grad=True)z = x.exp()np.testing.assert_array_almost_equal(z.data, np.exp([1, 2, 3]))z.backward([1, 1, 1])np.testing.assert_array_almost_equal(x.grad.data, np.exp([1, 2, 3]))

class Exp(_Function):def forward(ctx, x: ndarray) -> ndarray:ctx.save_for_backward(x)return np.exp(x)def backward(ctx, grad: ndarray) -> ndarray:x, = ctx.saved_tensorsreturn np.exp(x)

实现Pow运算

from core.tensor import Tensordef test_simple_pow():x = Tensor(2, requires_grad=True)y = 2z = x ** yassert z.data == 4z.backward()assert x.grad.data == 4def test_array_pow():x = Tensor([1, 2, 3], requires_grad=True)y = 3z = x ** yassert z.data.tolist() == [1, 8, 27]z.backward([1, 1, 1])assert x.grad.data.tolist() == [3, 12, 27]

class Pow(_Function):def forward(ctx, x: ndarray, c: ndarray) -> ndarray:ctx.save_for_backward(x, c)return x ** cdef backward(ctx, grad: ndarray) -> Tuple[ndarray, None]:x, c = ctx.saved_tensors# 把c当成一个常量,不需要计算梯度return grad * c * x ** (c - 1), None

实现y=xcy = x^cy=xc,这里ccc看成是常量,变量是xxx。常量ccc不需要计算梯度,我们返回None即可。

实现取负数

其实就是加一个负号y = -x

import numpy as npfrom core.tensor import Tensordef test_simple_exp():x = Tensor(2, requires_grad=True)z = -x  # -2assert z.data == -2z.backward()assert x.grad.data == -1def test_array_exp():x = Tensor([1, 2, 3], requires_grad=True)z = -xnp.testing.assert_array_equal(z.data, [-1, -2, -3])z.backward([1, 1, 1])np.testing.assert_array_equal(x.grad.data, [-1, -1, -1])

class Neg(_Function):def forward(ctx, x: ndarray) -> ndarray:return -xdef backward(ctx, grad: ndarray) -> ndarray:return -grad

总结

本文实现了常见运算的计算图,下篇文章会实现剩下的诸如求最大值、切片、变形和转置等运算。

从零实现深度学习框架——实现常见运算的计算图(上)相关推荐

  1. python学习框架图-从零搭建深度学习框架(二)用Python实现计算图和自动微分

    我们在上一篇文章<从零搭建深度学习框架(一)用NumPy实现GAN>中用Python+NumPy实现了一个简单的GAN模型,并大致设想了一下深度学习框架需要实现的主要功能.其中,不确定性最 ...

  2. 从零实现深度学习框架——GloVe从理论到实战

    引言 本着"凡我不能创造的,我就不能理解"的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导.

  3. 从零实现深度学习框架——Seq2Seq从理论到实战【实战】

    引言 本着"凡我不能创造的,我就不能理解"的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导.

  4. 从零实现深度学习框架——RNN从理论到实战【理论】

    引言 本着"凡我不能创造的,我就不能理解"的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导.

  5. 从零实现深度学习框架——深入浅出Word2vec(下)

    引言 本着"凡我不能创造的,我就不能理解"的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导. 要深入理解深度学 ...

  6. 从零实现深度学习框架——从共现矩阵到点互信息

    引言 本着"凡我不能创造的,我就不能理解"的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导.

  7. 从零实现深度学习框架——LSTM从理论到实战【理论】

    引言 本着"凡我不能创造的,我就不能理解"的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导.

  8. Win10深度学习框架及GPU运算环境搭建(CUDA10.0+Cudnn 7.6.5+pytroch1.2+tensorflow 1.14.0)

    一.深度学习为什么要搭建GPU运算环境? 熟悉深度学习的人都知道,深度学习是需要训练的,所谓的训练就是在成千上万个变量中寻找最佳值的计算,所需计算的数据量异常庞大.CPU是一个有多种功能的优秀领导者. ...

  9. 让 PyTorch 更轻便,这款深度学习框架你值得拥有!在 GitHub 上斩获 6.6K 星

    白交 发自 凹非寺  量子位 报道 | 公众号 QbitAI 一直以来,PyTorch就以简单又好用的特点,广受AI研究者的喜爱. 但是,一旦任务复杂化,就可能会发生一系列错误,花费的时间更长. 于是 ...

  10. 让PyTorch更轻便,这款深度学习框架你值得拥有!在GitHub上斩获6.6k星

    白交 发自 凹非寺  量子位 报道 | 公众号 QbitAI 一直以来,PyTorch就以简单又好用的特点,广受AI研究者的喜爱. 但是,一旦任务复杂化,就可能会发生一系列错误,花费的时间更长. 于是 ...

最新文章

  1. Java中的文件上传2(Commons FileUpload:commons-fileupload.jar)
  2. 千亿级照片,毫秒间匹配最佳结果,微软开源Bing搜索背后的关键算法
  3. 为什么说 Java 中只有值传递?
  4. 读书笔记《锋利的jQuery》
  5. 上传图片时,图片大小不一,设置宽高,图片拉伸,如何处理呢,我来告诉你...
  6. 实战:遇到HTM的文件图标丢失的问题
  7. Linux网络-数据包的接收流程(基于RTL8139网卡驱动程序)
  8. Vue Router history模式的配置方法及其原理
  9. mysql mybatis 主键id_MyBatis+MySQL 返回插入的主键ID
  10. (day 47 - 位运算 ) 剑指 Offer 65. 不用加减乘除做加法
  11. Atitit 文档资料处理重要类库与工具 跨语言api和第三方api跨语言 类库工具 大概功能 功能 Curl httpclient 文件上传下载 数据传输rest doctotext.exe
  12. c语言编程马克思手稿 百例,清华大学出版社-图书详情-《C语言趣味编程100例》...
  13. drawerLayout 抽屉的另一半,设置为透明的颜色
  14. 计算机考研404是什么意思,研路分享:我的404分考研高分心得体会
  15. java中long最大值源码表示_通过JDK源码角度分析Long类详解
  16. 机器学习实战——绘制决策树(代码)
  17. 百度想象空间还有多少?
  18. php文件上传代码dsn,数据源名称无效-使用php、PDO和DSN连接到MySQL
  19. python趣味编程-python趣味入门——写几个常玩的游戏
  20. 厦门吾智美:ESS.OIL.CO潜力无限

热门文章

  1. 通过计划任务使FlashFXP在晚上自动下载备份
  2. Altium Designer(四):敷铜设置
  3. 【Python】- scrapy 爬取图片保存到本地、且返回保存路径
  4. Jmeter打开url时提示“请在微信客户端打开链接问题”
  5. Quartz的时间配置
  6. ubuntu 切换java环境,配置单独的用户环境
  7. Blue Jeans - POJ 3080(多串的共同子串)
  8. 我的github教程
  9. JAVA 线程状态以及synchronized,wait,sleep,yield,notify,notifyAll
  10. C#代码与javaScript函数的相互调用(转)