当前位置:首页> AI教程> 从零手写Resnet50实战-搭建一个可以完成图片分类的神经网络

从零手写Resnet50实战-搭建一个可以完成图片分类的神经网络

释放双眼,带上耳机,听听看~!
本文章从零开始手写Resnet50实战,搭建一个可以完成图片分类的神经网络,不依赖第三方库,完全自主可控的手写算法。

大家好啊,我是董董灿。

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

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

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

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

从零手写Resnet50实战-搭建一个可以完成图片分类的神经网络

项目地址:从零手写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 的结构,搭出神经网络来了。

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

本文为作者原创,请勿转载,转载请联系作者。欢迎关注同名微信公众号:董董灿是个攻城狮

本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

机器学习和深度学习中的特征提取方法及示例代码

2023-12-21 8:24:14

AI教程

如何使用无知之幕来确定AI系统的公平原则

2023-12-21 8:38:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索