从零手写Resnet50实战—手写龟速卷积

简介: 从零手写Resnet50实战—手写龟速卷积

大家好啊,我是董董灿。

这是从零手写Resnet50实战的第3篇文章。

请跟着我的思路,一点点地手动搭建一个可以完成图片分类的神经网络,而且不依赖第三方库,完全自主可控的手写算法。

如对网络中的算法和原理不太熟悉,请移步万字长文解析Resnet50的算法原理。

我的目标是,识别出下面的这张图片是一只猫:

image.png

项目地址:从零手写resnet50。

正文

上一篇文章[我们已经把Resnet50中的所有权值参数都保存到txt中了。

接下来,把上图中猫的图片导入到内存中。

# 使用 Pillar 库来导入图片
# 仅使用该库导入图片
# 不使用该库进行任何其他的计算操作
from PIL import Image

# 读打开图片并读入到 img 中
img = Image.open('../cat.jfif')
# 将图片resize成长宽为(224,224)的图片
img = img.resize((224, 224))

这里说一下为什么要做 resize?

因为 Resnet50 首层卷积,接收的是一个 3 通道的图片数据。如果图片长宽太大,会使得卷积运算量过大,运行速度很慢,这一点在本文后面的实验可以看出来。

而且在试验 Resnet50 中,一个很常见的使用小图做运算的方法,便是将不规则大小的图片 resize 成(224,224)。其中两个 224 分别代表图片的长和宽,3 代表图片有 3 个通道。

在将图片导入到内存中之后,剩下的就是要将图片数据输入到神经网络中。但在此,需要先将核心算法完成,才能搭建成神经网络。

手写算法之——卷积

在Resnet50中,存在 6 种算法,分别是

  1. 卷积(Convolution,Conv)
  2. 批归一化(Batch Normal,BN)
  3. 池化(Pooling)
  4. 激活(Relu)
  5. 加法(Add)
  6. 全连接(Fully Connected, FC)

其中,Conv 和 FC 可以看作一类:都是在某些维度做乘累加计算;Pooling与卷积类似,只不过少了channel维度的累加。

BN是对输入数据的做批归一化操作,算法实现也不太难;而激活和加法就更简单了,属于两行代码就能搞定的算法。

所以,擒贼先擒王,先手写一个卷积算法试试水。

# 使用NHWC的 layout 来计算
# 卷积暂时不考虑 dilation 的存在
# 因为Resnet50中的卷积都不带 dilation 参数
def my_conv2d(img, weight, hi, wi, ci, co, kernel, stride, pad):
  '''
  img:输入图片数据
  weight:权值数据(卷积核)
  hi:输入图片高度-height
  wi:输入图片宽度-width
  ci:输入图片通道-channel,与weight的channel一致
  co:输出图片通道-channle,与weight的个数一致
  kernel:卷积核的大小
  stride:卷积核在输入图片上滑动步长
  pad:输入图片周围补充的pad值
  '''
  # 通过输入参数计算输出图片的长和宽
  # 在 Resnet50 中,卷积核在 h 方向和 w 方向的
  # 尺寸都是一样的,pad 也都是一样的,因此,
  # 这里用一个值来代表。
  ho = (hi + 2 * pad - kernel) // stride + 1
  wo = (wi + 2 * pad - kernel) // stride + 1

  # 将权值数据 reshape 成 co, kh, kw, ci 的形式
  weight = np.array(weight).reshape(co, kernel, kernel, ci)
  # 在输入图片周围补充pad值
  img_pad = np.pad(img, ((pad, pad), (pad, pad), (0, 0)), 'constant')
  # 初始化输出图片
  img_out = np.zeros((ho, wo, co))

  # 下面是卷积计算的核心逻辑
  # 其效果类似于 nn.conv2d
  for co_ in range(co):
    for ho_ in range(ho):
      in_h_origin = ho_ * stride - pad
      for wo_ in range(wo):
        in_w_origin = wo_ * stride - pad
        filter_h_start = max(0, -in_h_origin)
        filter_w_start = max(0, -in_w_origin)
        filter_h_end = min(kernel, hi - in_h_origin)
        filter_w_end = min(kernel, wi - in_w_origin)
        acc = float(0)
        for kh_ in range(filter_h_start, filter_h_end):
          hi_index = in_h_origin + kh_
          for kw_ in range(filter_w_start, filter_w_end):
            wi_index = in_w_origin + kw_
            for ci_ in range(ci):
              in_data = img[hi_index][wi_index][ci_]
              weight_data = weight[co_][kh_][kw_][ci_]
              acc = acc + in_data * weight_data
         img_out[ho_][wo_][co_] = acc
  return img_out

上面是手写的一个卷积算法,采用了最原始的堆叠循环的方式,没有对算法做任何的优化。

之所以这么写,是因为这样可以很清晰地看到卷积的计算过程。

将图片输入给卷积进行运算

在定义完上述卷积运算后,就可以将上一步导入的图片,输入给卷积,计算一下试试水了。

# 读入图片并转换为指定大小
img = Image.open('../cat.jfif')
img = img.resize((224, 224))

# 将Pillow Image对象转换为numpy数组
# data is layout as NHWC
out = np.array(img)

# 这个函数用来从保存的权值文件中读取权值数据
def get_weight_from_file(f):
  k = []
  with open(f, 'r') as f_:
    lines = f_.readlines()
    for l in lines:
      k.append(float(l))
return k

import datetime

# resnet50 第一次卷积的权值保存在项目中的路径
file_name = "../model_parser/dump_txt/resnet50_conv1_weight.txt"
# 将权值加载到内存中,赋值给K
k = get_weight_from_file(file_name)
# 打印当前时间戳
print(datetime.datetime.now())
# 调用手写的卷积进行计算,输出卷积结果
out = my_conv2d(out, k, 224, 224, 3, 64, 7, 2, 3)
# 打印计算完成的时间戳
print(datetime.datetime.now())
# 打印卷积计算结果的 shape
print(out.shape)

上面在调用 my_conv2d 之前,加了两个时间戳打印,看一下这个卷积运算的耗时。

$ 2023-04-13 08:21:20.473301
$ 2023-04-13 08:23:00.855593

从时间戳上可以看到,两个时间戳之间的间隔在1分多钟,说明这个卷积运算消耗了1分多钟,这可能与我用的虚拟机配置很低有关。

不过这种循环堆叠的卷积实现方式,很耗时是真的。

在卷积运算完之后,把这一层的输出的 shape 也打印出来。

$ (112, 112, 64)

可以看到,卷积的输出 shape 为 (112, 112, 64),通道数由输入图片的 3 通道变成了 64 通道,是因为使用了 64 个卷积核。

这里64个通道,实际上可以理解为这一层卷积在原始输入图片的像素之间,抽取出了 64 个特征出来。
至于是什么特征,我也不知道,有可能是猫的鼻子和耳朵。

至于上面说的卷积运算耗时的问题,暂时先不管他。在完成整网的推理,正确识别出来猫之后,我会继续将算法都优化一遍的。

后面还有 Pooling, Bn 算法的手写,写完之后,就可以按照 Resnet50 的结构,搭出神经网络来了。

今天,离识别出来猫,又进了一步。

本文为作者原创,请勿转载,转载请联系作者

相关文章
|
2天前
|
自然语言处理 PyTorch 算法框架/工具
自然语言生成任务中的5种采样方法介绍和Pytorch代码实现
在自然语言生成任务(NLG)中,采样方法是指从生成模型中获取文本输出的一种技术。本文将介绍常用的5中方法并用Pytorch进行实现。
130 0
|
7月前
|
机器学习/深度学习 PyTorch 算法框架/工具
PyTorch应用实战一:实现卷积操作
PyTorch应用实战一:实现卷积操作
94 0
|
机器学习/深度学习 计算机视觉
用实验数据验证面试题:VGG使用3x3卷积核的优势
用实验数据验证面试题:VGG使用3x3卷积核的优势
405 0
用实验数据验证面试题:VGG使用3x3卷积核的优势
|
9月前
|
机器学习/深度学习 算法
什么是卷积?(含如何实现卷积源码)
什么是卷积?(卷积简单实现源码)
87 0
什么是卷积?(含如何实现卷积源码)
|
机器学习/深度学习 算法 Java
|
机器学习/深度学习 存储 算法
|
机器学习/深度学习 存储 算法
从零手写Resnet50实战——权值另存为
从零手写Resnet50实战——权值另存为
|
机器学习/深度学习 数据挖掘 PyTorch
# 【深度学习】:《PyTorch入门到项目实战》(十一):卷积层
>之前已经介绍了基本的神经网络知识以及一些处理过拟合欠拟合的概念。现在我们正式进入卷积神经网络的学习。CNN是⼀类强⼤的、为处理图像数据⽽设计的神经⽹络。基于卷积神经⽹络架构的模型在计算机视觉领域中已经占主导地位,当今⼏乎所有的图像识别、⽬标检测或语义分割相关的学术竞赛和商业应⽤都以这种⽅法为基础。对于计算机视觉而言,面临的一个重大挑战就是数据的输入可能会很大。例如,我们有一张64$\times$ 64的图片,假设通道数为3,那么它是数据量相当于是一个$64\times 64\times 3=12288$的特征向量。当我们要操作更大的图片时候,需要进行卷积计算,它是卷积神经网络中非常重要的一部
 # 【深度学习】:《PyTorch入门到项目实战》(十一):卷积层
|
机器学习/深度学习
【深度学习】Dropout原理以及代码实现
【深度学习】Dropout原理以及代码实现
101 0
【深度学习】Dropout原理以及代码实现
|
机器学习/深度学习 算法框架/工具 计算机视觉
经典神经网络 | ResNet 论文解析及代码实现
经典神经网络 | ResNet 论文解析及代码实现
经典神经网络 | ResNet 论文解析及代码实现