深圳幻海软件技术有限公司 欢迎您!

Tensorflow深度可分离卷积实战

2023-02-28

译者| 朱先忠审校| 孙淑娟归纳一下所有当今巨型卷积神经网络(例如RESNET、VGG等),它们都引出了同样一个问题:我们如何能够用更少的参数使所有这些网络体积更小,同时仍然保持相同的精度水平,甚至使用更少的参数改进模型的泛化。一种方法是借助于深度可分离卷积,也称为TensorF

译者 | 朱先忠

审校 | 孙淑娟

归纳一下所有当今巨型卷积神经网络(例如RESNET、VGG等),它们都引出了同样一个问题:我们如何能够用更少的参数使所有这些网络体积更小,同时仍然保持相同的精度水平,甚至使用更少的参数改进模型的泛化。

一种方法是借助于深度可分离卷积,也称为TensorFlow和Pytorch中的可分离卷积(不要与空间可分离卷积混淆,后者也称为可分离卷积)。深度可分离卷积是由Sifre在论文《用于图像分类的刚性运动散射》(Rigid-motion scattering for image classification)中引入的,并且已被当下流行的模型架构(如MobileNet)和类似版本的Exception所采用,主要用于分割通常在正常卷积层中组合在一起的信道和空间卷积。

在本教程中,我们将研究什么是深度可分离卷积,以及如何使用它们来加速卷积神经网络图像模型的计算。

完成本教程后,您将学习到:

  • 什么是Depthwise(逐层)、Pointwise(逐像素)和深度可分离卷积
  • 如何在Tensorflow中实现深度可分离卷积
  • 使用它们作为我们计算机视觉模型的一部分

APPbpv">让我们开始吧!

Tensorflow中使用深度可分离卷积

概述

本教程将分为三部分展开:

  • 什么是深度可分离卷积
  • 为什么它们如此有用
  • 深度可分离卷积在计算机视觉模型中的应用

什么是深度可分离卷积

在深入研究深度(Depthwise)和深度可分离卷积之前,不妨先让我们快速回顾一下卷积概念,这可能会对下文的理解更有帮助。图像处理中的卷积是在体积上应用核的过程,该过程中我们要对像素进行加权求和,并将权重作为核的值,如下图所示:


在10x10x3输入体积上应用3×3内核可输出8x1体积

现在,让我们介绍一下深度卷积。深度卷积基本上是仅沿图像的一个空间维度的卷积。从视觉角度来看,可以使用下图来描述单个深度卷积滤波器的外观和功能:

在10x10x3输入体积上应用深度卷积滤波器,输出8x8x3体积

正常卷积层和深度卷积之间的关键区别在于,深度卷积仅沿一个空间维度(即通道)应用卷积,而正常卷积是在每个步骤在所有空间维度/通道上应用卷积运算。

如果我们看一看整个深度(Depthwise)层在所有RGB通道上的如下图所示操作:

在10x10x3输入体积上应用深度卷积滤波器,输出8x8x3体积

那么我们会注意到,因为我们对每个输出通道应用一个卷积滤波器;所以,输出通道的数量等于输入通道的数量。在应用这个深度卷积层之后,我们再应用一个逐像素(Pointwise)卷积层。

简单地说,逐像素(Pointwise)卷积层是具有1x1内核的常规卷积层(因此可以透过所有通道来查看单个像素信息)。从视觉效果上来看的话,它看起来像这样:

在10x10x3输入体积上应用逐点卷积可输出10x10x1输出体积

为什么深度可分离卷积如此有用?

现在,你可能想知道:用深度可分卷积做两次运算有什么作用?鉴于本文的标题是关于加速计算机视觉模型的讨论,那么,如何通过两次操作而不是一次操作来加快速度?

为了回答这个问题,让我们看看模型中的参数数量(不过,还有与进行两次卷积而不是一次卷积相关的额外开销)。假设我们想对RGB图像应用64个卷积滤波器,使输出中有64个通道。正常卷积层中的参数数量(包括偏置项)为3x3x3x64+64=1792。另一方面,使用深度可分离卷积层时,只有(3x3x1x3+3)+(1x1x64+64)=30+256=286个参数,这是一个显著的减少,深度可分离的卷积的参数小于正常卷积的6倍。

因此,引入深度可分离卷积层将有助于减少计算和参数的数量,从而减少训练/推理时间,并有助于分别正则化我们的模型。

让我们看一看实战情形吧。对于我们提供的输入,让我们使用32x32x3图像的CIFAR10图像数据集进行训练。关键代码片断如下所示:

import tensorflow.keras as keras
from keras.datasets import mnist

#加载数据集
(trainX, trainY), (testX, testY) = keras.datasets.cifar10.load_data()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

接下来,我们将自己实现一个深度可分离的卷积层。其实,Tensorflow中已经有一个实现,但是我们将在最后一个示例代码中再讨论它。

class DepthwiseSeparableConv2D(keras.layers.Layer):
  def __init__(self, filters, kernel_size, padding, activation):
    super(DepthwiseSeparableConv2D, self).__init__()
    self.Depthwise = DepthwiseConv2D(kernel_size = kernel_size, padding = padding, activation = activation)
    self.Pointwise = Conv2D(filters = filters, kernel_size = (1, 1), activation = activation)

  def call(self, input_tensor):
    x = self.Depthwise(input_tensor)
    return self.Pointwise(x)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

然后,我们使用深度可分离卷积层构造模型并查看参数的数量:

visible = Input(shape=(32, 32, 3))
Depthwise_separable = DepthwiseSeparableConv2D(filters=64, kernel_size=(3,3), padding="valid", activation="relu")(visible)
Depthwise_model = Model(inputs=visible, outputs=Depthwise_separable)
Depthwise_model.summary()
  • 1.
  • 2.
  • 3.
  • 4.

上述设计将导致如下输出结果:

_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_15 (InputLayer)       [(None, 32, 32, 3)]       0         
                                                                 
 Depthwise_separable_conv2d_  (None, 30, 30, 64)       286       
 11 (DepthwiseSeparableConv2                                     
 D)                                                              

=================================================================
Total params: 286
Trainable params: 286
Non-trainable params: 0
_________________________________________________________________
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

我们可以将其与使用常规2D卷积层的类似模型进行比较:

normal = Conv2D(filters=64, kernel_size=(3,3), padding=”valid”, activation=”relu”)(visible)
  • 1.

如此一来,这将导致如下输出结果:

_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input (InputLayer)       [(None, 32, 32, 3)]       0         
                                                                 
 conv2d (Conv2D)          (None, 30, 30, 64)        1792      
                                                                 
=================================================================
Total params: 1,792
Trainable params: 1,792
Non-trainable params: 0
_________________________________________________________________
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

这与我们先前对参数数量的初步计算相吻合,并显示了使用深度可分离卷积可以减少参数数量。

更具体地说,让我们看看普通卷积层和深度可分离层中核的数量和大小。当我们观察一个常规的二维卷积层(其中,c通道作为输入,w x h是内核空间分辨率,n通道作为输出)时,我们需要有(n,w,h,c)形式的参数,即n个滤波器,每个滤波器的内核大小为(w,h,c)。然而,对于类似的深度可分离卷积,即使输入通道数、内核空间分辨率和输出通道数与之相同,情况也不同。

首先,深度卷积涉及c个滤波器,每个滤波器的内核大小为(w,h,1),它输出c个通道,因为它作用于每个滤波器。该深度卷积层具有(c,w,h,1)形式的参数(加上一些偏置单元)。然后是逐像素卷积,它从深度层接收c个通道,并输出n个通道,因此我们有n个滤波器,每个滤波器的内核大小为(1,1,n)。该逐像素卷积层具有(n,1,1,n)样子的参数(加上一些偏置单元)。

你现在可能在想:它们为什么起作用呢?

从Chollet的Exception论文中可以看出,深度可分离卷积假设我们可以分别映射交叉信道和空间相关性。鉴于此,卷积层中会有一堆冗余权重,我们可以通过将卷积分离为逐层分量和逐像素分量的两个卷积来减少这些冗余权重。对于熟悉线性代数的人来说,一种思考这个问题的方法是,当矩阵中的列向量是彼此的倍数时,我们如何将矩阵分解为两个向量的外积。

在计算机视觉模型领域使用深度可分离卷积

现在,我们已经看到了通过使用深度可分离卷积而不是普通卷积滤波器可以使计算所需要的参数减少。接下来让我们看看如何将其应用于Tensorflow的可分离二维滤波器。

对于本例来说,我们将使用上述示例中使用的CIFAR-10图像数据集,而对于模型,我们将采用基于VGG块构建的模型。深度可分离卷积的潜力存在于更深层次的模型中;其中,正则化效应对模型更有利,且参数减少更明显,而不是像LeNet-5这样的相对轻量级权重模型一样。

现在,我们将使用VGG块和普通卷积层来创建我们的模型,相关代码如下:

from keras.models import Model
from keras.layers import Input, Conv2D, MaxPooling2D, Dense, Flatten, SeparableConv2D
import tensorflow as tf

#创建VGG块的函数
def vgg_block(layer_in, n_filters, n_conv):
  # 添加卷积层
  for _ in range(n_conv):
    layer_in = Conv2D(filters = n_filters, kernel_size = (3,3), padding='same', activation="relu")(layer_in)
  #添加最大池化层
  layer_in = MaxPooling2D((2,2), strides=(2,2))(layer_in)
  return layer_in

visible = Input(shape=(32, 32, 3))
layer = vgg_block(visible, 64, 2)
layer = vgg_block(layer, 128, 2)
layer = vgg_block(layer, 256, 2)
layer = Flatten()(layer)
layer = Dense(units=10, activation="softmax")(layer)

#创建模型
model = Model(inputs=visible, outputs=layer)

#总结模型
model.summary()

model.compile(optimizer="adam", loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics="acc")

history = model.fit(x=trainX, y=trainY, batch_size=128, epochs=10, validation_data=(testX, testY))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

然后,我们看一下这个具有正常卷积层的6层卷积神经网络的输出结果:

_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 32, 32, 3)]       0         
                                                                 
 conv2d (Conv2D)             (None, 32, 32, 64)        1792                                                                       

 conv2d_1 (Conv2D)           (None, 32, 32, 64)        36928     
                                                                 
 max_pooling2d (MaxPooling2D  (None, 16, 16, 64)       0         
 )                                                               
                                                                 
 conv2d_2 (Conv2D)           (None, 16, 16, 128)       73856     
                                                                 
 conv2d_3 (Conv2D)           (None, 16, 16, 128)       147584                                                                     

 max_pooling2d_1 (MaxPooling  (None, 8, 8, 128)        0         
 2D)                                                             
                                                                 
 conv2d_4 (Conv2D)           (None, 8, 8, 256)         295168    
                                                                 
 conv2d_5 (Conv2D)           (None, 8, 8, 256)         590080    
                                                                 
 max_pooling2d_2 (MaxPooling  (None, 4, 4, 256)        0         
 2D)                                                             
                                                                 
 flatten (Flatten)           (None, 4096)              0         
                                                                 
 dense (Dense)               (None, 10)                40970     
                                                                 
=================================================================
Total params: 1,186,378
Trainable params: 1,186,378
Non-trainable params: 0
_________________________________________________________________
Epoch 1/10
391/391 [==============================] - 11s 27ms/step - loss: 1.7468 - acc: 0.4496 - val_loss: 1.3347 - val_acc: 0.5297
Epoch 2/10
391/391 [==============================] - 10s 26ms/step - loss: 1.0224 - acc: 0.6399 - val_loss: 0.9457 - val_acc: 0.6717
Epoch 3/10
391/391 [==============================] - 10s 26ms/step - loss: 0.7846 - acc: 0.7282 - val_loss: 0.8566 - val_acc: 0.7109
Epoch 4/10
391/391 [==============================] - 10s 26ms/step - loss: 0.6394 - acc: 0.7784 - val_loss: 0.8289 - val_acc: 0.7235
Epoch 5/10
391/391 [==============================] - 10s 26ms/step - loss: 0.5385 - acc: 0.8118 - val_loss: 0.7445 - val_acc: 0.7516
Epoch 6/10
391/391 [==============================] - 11s 27ms/step - loss: 0.4441 - acc: 0.8461 - val_loss: 0.7927 - val_acc: 0.7501
Epoch 7/10
391/391 [==============================] - 11s 27ms/step - loss: 0.3786 - acc: 0.8672 - val_loss: 0.8279 - val_acc: 0.7455
Epoch 8/10
391/391 [==============================] - 10s 26ms/step - loss: 0.3261 - acc: 0.8855 - val_loss: 0.8886 - val_acc: 0.7560
Epoch 9/10
391/391 [==============================] - 10s 27ms/step - loss: 0.2747 - acc: 0.9044 - val_loss: 1.0134 - val_acc: 0.7387
Epoch 10/10
391/391 [==============================] - 10s 26ms/step - loss: 0.2519 - acc: 0.9126 - val_loss: 0.9571 - val_acc: 0.7484
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.

让我们尝试相同的架构,但用Keras的SeparableConv2D层代替普通卷积层:

# 深度可分离VGG块
def vgg_Depthwise_block(layer_in, n_filters, n_conv):
  # 添加卷积层
  for _ in range(n_conv):
    layer_in = SeparableConv2D(filters = n_filters, kernel_size = (3,3), padding='same', activation='relu')(layer_in)
  # 添加最大池化层
  layer_in = MaxPooling2D((2,2), strides=(2,2))(layer_in)
  return layer_in

visible = Input(shape=(32, 32, 3))
layer = vgg_Depthwise_block(visible, 64, 2)
layer = vgg_Depthwise_block(layer, 128, 2)
layer = vgg_Depthwise_block(layer, 256, 2)
layer = Flatten()(layer)
layer = Dense(units=10, activation="softmax")(layer)
#创建模型
model = Model(inputs=visible, outputs=layer)

# 总结模型
model.summary()

model.compile(optimizer="adam", loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics="acc")

history_dsconv = model.fit(x=trainX, y=trainY, batch_size=128, epochs=10, validation_data=(testX, testY))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

运行上述代码可以得到以下结果:

_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 32, 32, 3)]       0         
                                                                 
 separable_conv2d (Separab  (None, 32, 32, 64)       283       
leConv2D)                                                       
                                                                 
 separable_conv2d_2 (Separab  (None, 32, 32, 64)       4736      
 leConv2D)                                                       
                                                                 
 max_pooling2d (MaxPoolin  (None, 16, 16, 64)       0         
 g2D)                                                            
                                                                 
 separable_conv2d_3 (Separab  (None, 16, 16, 128)      8896      
 leConv2D)                                                       
                                                                 
 separable_conv2d_4 (Separab  (None, 16, 16, 128)      17664     
 leConv2D)                                                       
                                                                 
 max_pooling2d_2 (MaxPoolin  (None, 8, 8, 128)        0         
 g2D)                                                            
                                                                 
 separable_conv2d_5 (Separa  (None, 8, 8, 256)        34176     
 bleConv2D)                                                      
                                                                 
 separable_conv2d_6 (Separa  (None, 8, 8, 256)        68096     
 bleConv2D)                                                      
                                                                 
 max_pooling2d_3 (MaxPoolin  (None, 4, 4, 256)        0         
 g2D)                                                            
                                                                 
 flatten (Flatten)         (None, 4096)              0         
                                                                 
 dense (Dense)             (None, 10)                40970     
                                                                 
=================================================================
Total params: 174,821
Trainable params: 174,821
Non-trainable params: 0
_________________________________________________________________
Epoch 1/10
391/391 [==============================] - 10s 22ms/step - loss: 1.7578 - acc: 0.3534 - val_loss: 1.4138 - val_acc: 0.4918
Epoch 2/10
391/391 [==============================] - 8s 21ms/step - loss: 1.2712 - acc: 0.5452 - val_loss: 1.1618 - val_acc: 0.5861
Epoch 3/10
391/391 [==============================] - 8s 22ms/step - loss: 1.0560 - acc: 0.6286 - val_loss: 0.9950 - val_acc: 0.6501
Epoch 4/10
391/391 [==============================] - 8s 21ms/step - loss: 0.9175 - acc: 0.6800 - val_loss: 0.9327 - val_acc: 0.6721
Epoch 5/10
391/391 [==============================] - 9s 22ms/step - loss: 0.7939 - acc: 0.7227 - val_loss: 0.8348 - val_acc: 0.7056
Epoch 6/10
391/391 [==============================] - 8s 22ms/step - loss: 0.7120 - acc: 0.7515 - val_loss: 0.8228 - val_acc: 0.7153
Epoch 7/10
391/391 [==============================] - 8s 21ms/step - loss: 0.6346 - acc: 0.7772 - val_loss: 0.7444 - val_acc: 0.7415
Epoch 8/10
391/391 [==============================] - 8s 21ms/step - loss: 0.5534 - acc: 0.8061 - val_loss: 0.7417 - val_acc: 0.7537
Epoch 9/10
391/391 [==============================] - 8s 21ms/step - loss: 0.4865 - acc: 0.8301 - val_loss: 0.7348 - val_acc: 0.7582
Epoch 10/10
391/391 [==============================] - 8s 21ms/step - loss: 0.4321 - acc: 0.8485 - val_loss: 0.7968 - val_acc: 0.7458
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.

请注意,深度可分离卷积版本中的参数明显较少(~200k vs~1.2m参数),每个迭代单元的训练时间略少。深度可分离卷积更可能在可能面临过拟合问题的深层模型和具有较大核的层上工作得更好,因为参数和计算的减少会抵消进行两次卷积而不是一次卷积的额外计算成本。接下来,我们绘制两个模型的训练、验证和准确性,以查看模型训练性能的差异:

正常卷积层网络的训练与验证精度

深度可分离卷积层网络的训练与验证精度

从上图中的对比情况来看,两种模型的最高验证精度相似,但深度可分离卷积似乎对序列集的过度拟合较少,这可能有助于更好地推广到新数据。

对于模型的深度可分离卷积版本,将所有代码组合在一起,就形成如下所示代码:

import tensorflow.keras as keras
from keras.datasets import mnist

#加载数据集
(trainX, trainY), (testX, testY) = keras.datasets.cifar10.load_data()
# 深度可分离VGG块
def vgg_Depthwise_block(layer_in, n_filters, n_conv):
  #添加卷积层
  for _ in range(n_conv):
    layer_in = SeparableConv2D(filters = n_filters, kernel_size = (3,3), padding='same',activation='relu')(layer_in)
  # 添加最大池化层
  layer_in = MaxPooling2D((2,2), strides=(2,2))(layer_in)
  return layer_in

visible = Input(shape=(32, 32, 3))
layer = vgg_Depthwise_block(visible, 64, 2)
layer = vgg_Depthwise_block(layer, 128, 2)
layer = vgg_Depthwise_block(layer, 256, 2)
layer = Flatten()(layer)
layer = Dense(units=10, activation="softmax")(layer)
# 创建模型
model = Model(inputs=visible, outputs=layer)

#总结模型
model.summary()

model.compile(optimizer="adam", loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics="acc")

history_dsconv = model.fit(x=trainX, y=trainY, batch_size=128, epochs=10, validation_data=(testX, testY))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

推荐继续阅读资料

如果您想更深入地了解与本文主题相关的内容,本节将提供有关此主题的如下更多的参考资源。

论文方面:

  • ​​Rigid-Motion Scattering For Image Classification​​(深度可分卷积)
  • ​​MobileNet​​
  • ​​Xception​​

API函数库:

Depthwise Separable Convolutions in Tensorflow (SeparableConv2D):https://www.tensorflow.org/api_docs/python/tf/keras/layers/SeparableConv2D

总结

在这篇文章中,您了解了逐层、逐像素和深度可分离卷积等内容。您还学到了如何使用深度可分离卷积让我们在使用数量少得多的参数的同时获得有竞争力的训练结果。

归纳来看,通过本文您已经学会:

  • 什么是逐层、逐像素(Pointwise)和深度可分离卷积
  • 如何在Tensorflow中实现深度可分离卷积
  • 使用这些技术作为我们计算机视觉模型构建的一部分

[译者注]在深度可分离卷积(以及谷歌的Xception等)概念范围内,Depthwise(逐层)是一个常常与Pointwise(逐像素,或者干脆称“逐点”)对比提及的术语。但是,独立的单词Depthwise常常翻译为“深度”;例如Depthwise Separable Convolutions,通常翻译为“深度可分离卷积”。

译者介绍

朱先忠,51CTO社区编辑,51CTO专家博客、讲师,潍坊一所高校计算机教师,自由编程界老兵一枚。早期专注各种微软技术(编著成ASP.NET AJX、Cocos 2d-X相关三本技术图书),近十多年投身于开源世界(熟悉流行全栈Web开发技术),了解基于OneNet/AliOS+Arduino/ESP32/树莓派等物联网开发技术与Scala+Hadoop+Spark+Flink等大数据开发技术。

原文标题:​​Using Depthwise Separable Convolutions in Tensorflow​​,作者:Zhe Ming Chng