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

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

大家好啊,我是董董灿。

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

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

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

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

image.png

正文

上一篇文章[我们已经把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 的结构,搭出神经网络来了。

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

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

相关文章
|
Linux Anolis 异构计算
关于远程直接内存访问技术 RDMA 的高性能架构设计介绍
本文介绍 RDMA 技术的基本原理及交流在工程上的设计思路。
|
机器学习/深度学习 人工智能 PyTorch
|
SQL 安全 Java
理解Web安全:防止Java Web应用的安全漏洞
【4月更文挑战第3天】在互联网时代,Java Web应用面临严峻的安全挑战,包括注入攻击、XSS、CSRF等。为保护应用,需实施安全措施和最佳实践:如输入验证、输出编码、使用安全框架、防范CSRF、管理会话、加密数据、智能错误处理及定期安全审计。通过这些方法,可降低安全风险,但需持续关注安全趋势并适应新技术。
491 0
|
存储 文件存储
收藏帖!FreeNAS系统硬盘损坏更换教程,重组系统恢复数据
收藏帖!FreeNAS系统硬盘损坏更换教程,重组系统恢复数据
收藏帖!FreeNAS系统硬盘损坏更换教程,重组系统恢复数据
|
存储 数据采集 XML
再谈主数据管理|一文读懂主数据项目实施
主数据管理是企业改善其关键数据资产(如产品数据,资产数据,客户数据,位置数据等)的一致性和质量的必要数据管理活动。
|
3月前
|
算法 Go 计算机视觉
YOLOv8结构解读
YOLOv8是Ultralytics团队开发的最新目标检测模型,基于YOLO系列的高效单阶段架构,进一步优化了精度与速度。相比YOLOv5,YOLOv8在Backbone中移除了Focus模块,优化了CSP结构;在Neck部分增强了特征融合能力;Head部分采用Anchor-Free与解耦头设计,提升检测精度。此外,YOLOv8引入了更先进的损失函数、自对抗训练和动态标签分配等策略,显著提高了模型性能与鲁棒性。
1097 0
|
存储 人工智能 JSON
|
JavaScript 前端开发 UED
服务器端渲染新浪潮:用Vue.js和Nuxt.js构建高性能Web应用
【8月更文挑战第30天】在现代Web开发中,提升应用性能和SEO友好性是前端开发者面临的挑战。服务器端渲染(SSR)能加快页面加载速度并改善搜索引擎优化。Vue.js结合Nuxt.js提供了一个高效框架来创建SSR应用。通过安装`create-nuxt-app`,可以轻松创建新的Nuxt.js项目,并利用其自动路由功能简化页面管理。Nuxt.js默认采用SSR模式,并支持通过`asyncData`方法预取数据,同时提供了静态站点生成和服务器端渲染的部署选项,显著提升用户体验。
285 0
|
弹性计算 Shell 数据安全/隐私保护
动态管理Docker容器的自动扩展与缩减
【4月更文挑战第30天】
261 0
|
机器学习/深度学习 人工智能 算法
Resnet图像识别入门——残差结构
残差结构像是Resnet的告诉公路,可以将计算误差很好的保留下来。
Resnet图像识别入门——残差结构