基于NumPy和图像分类的人工神经网络构建
本文利用NumPy系统在Python中构建人工神经网络,以便为Fruits360数据集执行图像分类应用程序。
本文提及的所有内容(即图像和源代码,不包括Fruits360的图片)均来自于Ahmed Fawzy Gad 著作《Practical Computer Vision Applications Using Deep Learning with CNNs》。
本书可以在以下网址查阅:https://springer.com/us/book/9781484241660
本文中使用的源代码可以在以下网址查阅:
https://github.com/ahmedfgad/NumPyANN
Fruits360数据集中有60类水果,如苹果、番石榴、鳄梨、香蕉、樱桃、枣、猕猴桃、桃子等。为了简明扼要,本文只选定了四种分类,分别是苹果、柠檬、芒果和覆盆子。每种分类大约有491个图像用于操作,162个图像用于测试。图像大小为100x100像素。
特征提取
首先,选取一组合适的特征来保证分类的准确性。如下图所示的4个类别的样本图像,这些水果的颜色不同,因此颜色特征适合用于特征提取的工作任务。
RGB颜色空间不会将颜色信息与其他类型的信息(如亮度)区分开。因此,如果用RGB来表示图像,会有3个通道参与到计算中。出于此原因,最好使用将颜色信息隔离成单个通道(如HSV)的颜色空间。在这种情况下,颜色通道即是色调通道(H)。下图显示了之前呈现的4个样本的色调通道。我们可以注意到每个图像的色调值与其他图像的不同之处。
色调通道大小依旧为100x100。如果将整个通道应用于ANN,则输入层将具有10,000个神经元,网络数据量过大。为了减少使用的数据量,我们可以使用直方图来表示色调通道。直方图具有360个区间,反映色调值的数据。下图是4个样本图像的直方图,每种水果的色调值都落在直方图的某些特定区间中。与使用RGB颜色空间中的任何通道相比,不同类别之间的重叠较少。例如,芒果的色调在直方图中的区间为90~110,而苹果的色调区间则为0~10。每个种类之间的边距减少了分类的模糊性,从而提高了预测的准确性。
以下是根据4个图像计算色调通道直方图的代码:
import numpy
import skimage.io, skimage.color
import matplotlib.pyplot
raspberry=skimage.io.imread(fname="raspberry.jpg",as_grey=False)
apple=skimage.io.imread(fname="apple.jpg",as_grey=False)
mango = skimage.io.imread(fname="mango.jpg", as_grey=False)
lemon = skimage.io.imread(fname="lemon.jpg", as_grey=False)
apple_hsv = skimage.color.rgb2hsv(rgb=apple)
mango_hsv = skimage.color.rgb2hsv(rgb=mango)
raspberry_hsv=skimage.color.rgb2hsv(rgb=raspberry)
lemon_hsv = skimage.color.rgb2hsv(rgb=lemon)
fruits = ["apple", "raspberry", "mango", "lemon"]
hsv_fruits_data = [apple_hsv, raspberry_hsv, mango_hsv, lemon_hsv]
idx = 0
for hsv_fruit_data in hsv_fruits_data:
fruit = fruits[idx]
hist = numpy.histogram(a=hsv_fruit_data[:, :, 0], bins=360)
matplotlib.pyplot.bar(left=numpy.arange(360), height=hist[0])
matplotlib.pyplot.savefig(fruit+"-hue-histogram.jpg", bbox_inches="tight")
matplotlib.pyplot.close("all")
idx = idx + 1
通过循环使用4个图像类中的所有图像,我们可以从所有图像中提取特征(参见下面的代码)。根据4个分类中的图像数量(1962)和从每个图像中提取的特征向量长度(360),创建NumPy零数组并将其保存在dataset_features变量中。为了存储每个图像的类标签,创建另一个名为outputs的NumPy数组。apple的类标签为0,lemon为1,mango为2,raspberry为3。通过代码操作,使该数组在根目录中运行,其中有4个文件夹根据名为fruits的列表中列出的水果名称命名。该数组遍历所有文件夹中的所有图像,从每个图像中提取色调直方图,为每个图像分配一个类标签,最后使用pickle库保存提取的特征和类标签(也可以使用NumPy来保存生成的NumPy数组而不是pickle)。
import numpy
import skimage.io, skimage.color, skimage.feature
import os
import pickle
fruits = ["apple", "raspberry", "mango", "lemon"]
#492+490+490+490=1,962
dataset_features = numpy.zeros(shape=(1962, 360))
outputs = numpy.zeros(shape=(1962))
idx = 0
class_label = 0
for fruit_dir in fruits:
curr_dir = os.path.join(os.path.sep, fruit_dir)
all_imgs = os.listdir(os.getcwd()+curr_dir)
for img_file in all_imgs: fruit_data=skimage.io.imread(fname=os.getcwd()+curr_dir+img_file, as_grey=False)
fruit_data_hsv=skimage.color.rgb2hsv(rgb=fruit_data)
hist = numpy.histogram(a=fruit_data_hsv[:, :, 0], bins=360)
dataset_features[idx, :] = hist[0]
outputs[idx] = class_label
idx = idx + 1
class_label = class_label + 1
with open("dataset_features.pkl", "wb") as f:
pickle.dump("dataset_features.pkl", f)
with open("outputs.pkl", "wb") as f:
pickle.dump(outputs, f)
现在,用360个元素的特征向量来表示每个图像。过滤这些元素是为了保留最相关的元素以区分4个类。减少的特征向量长度是102而不是360。使用更少的元素能够更快地完成之前的操作。
dataset_features变量大小为1962x102。至此,相关数据(功能和类标签)已准备就绪。接下来是使用Numpy实现神经网络构建(ANN)。
人工神经网络构建(ANN)
下图展示了目标ANN结构。输入层有102个数据,2个隐藏层分别有150和60个神经元,输出层有4项(每个水果分类各一项)。
每个层的输入向量乘以(矩阵乘法)权重矩阵,连接到下一层产生输出向量。
这样的输出向量再次乘以权重矩阵,连接到下一层。该过程不断重复,直到到达输出层。矩阵乘法的总结如下图所示。
大小为1×102的输入向量乘以大小为102×150的第一个隐藏层的权重矩阵(矩阵乘法),得到输出矩阵为1×150。然后将该输出矩阵用作第二层隐藏层的输入,乘以大小为150×60的权重矩阵,得到输出矩阵1×60。最后,将该输出矩阵乘以60×4的权重矩阵,最终得到结果为1×4。这个结果向量中的每个元素都指向一个输出类。输入样本按照得分最高的类别进行标记。
上述操作过程的Python代码如下所示:
import numpy
import pickle
def sigmiod(inpt):
return 1.0 / (1 + numpy.exp(-1 * inpt))
f = open("dataset_features.pkl", "rb")
data_inputs2 = pickle.load(f)
f.close()
features_STDs = numpy.std(a=data_inputs2, axis=0)
data_inputs = data_inputs2[:, features_STDs > 50]
f = open("outputs.pkl", "rb")
data_outputs = pickle.load(f)
f.close()
HL1_neurons = 150
input_HL1_weights = numpy.random.uniform(low=-0.1, high=0.1,
size=(data_inputs.shape[1], HL1_neurons))
HL2_neurons = 60
HL1_HL2_weights = numpy.random.uniform(low=-0.1, high=0.1,
size=(HL1_neurons, HL2_neurons))
output_neurons = 4
HL2_output_weights = numpy.random.uniform(low=-0.1, high=0.1,
size=(HL2_neurons, output_neurons))
H1_outputs = numpy.matmul(a=data_inputs[0, :], b=input_HL1_weights)
H1_outputs = sigmoid(H1_outputs)
H2_outputs = numpy.matmul(a=H1_outputs, b=HL1_HL2_weights)
H2_outputs = sigmoid(H2_outputs)
out_otuputs = numpy.matmul(a=H2_outputs, b=HL2_output_weights)
predicted_label = numpy.where(out_otuputs == numpy.max(out_otuputs))[0][0]
print("Predicted class : ", predicted_label)
读取先前保存的特征及输出标签,并过滤特征之后,应定义各层的权重矩阵,随机赋予它们-0.1到0.1的值。例如,变量“input_HL1_weights”表示输入层和第一隐藏层之间的权重矩阵,根据特征元素的数量和隐藏层中神经元的数量来定义矩阵的大小。创建权重矩阵之后的下一步是应用矩阵乘法。
例如,变量“H1_outputs”代表给定样本的特征向量乘以输入层和第一隐藏层之间的权重矩阵所得到的输出。
通常来说,激活函数应用于每个隐藏层的输出,在输入和输出之间创建非线性关系。例如,sigmoid激活函数可以用于矩阵乘法的输出。
通常来说,激活函数应用于每个隐藏层的输出,在输入和输出之间创建非线性关系。例如,sigmoid激活函数可以用于矩阵乘法的输出。
在输出层的输出完成后,可以开始进行预测。预测的类标签保存在“predict_label”变量中。对每个输入样本重复上述步骤。
适用于所有样本的完整代码如下所示:
import numpy
import pickle
def sigmoid(inpt):
return 1.0 / (1 + numpy.exp(-1 * inpt))
def relu(inpt):
result = inpt
result[inpt < 0] = 0
return result
def update_weights(weights, learning_rate):
new_weights = weights - learning_rate * weights
return new_weights
def train_network(num_iterations, weights, data_inputs, data_outputs, learning_rate, activation="relu"):
for iteration in range(num_iterations):
print("Itreation ", iteration)
for sample_idx in range(data_inputs.shape[0]):
r1 = data_inputs[sample_idx, :]
for idx in range(len(weights) - 1):
curr_weights = weights[idx]
r1 = numpy.matmul(a=r1, b=curr_weights)
if activation == "relu":
r1 = relu(r1)
elif activation == "sigmoid":
r1 = sigmoid(r1)
curr_weights = weights[-1]
r1 = numpy.matmul(a=r1, b=curr_weights)
predicted_label = numpy.where(r1 == numpy.max(r1))[0][0]
desired_label = data_outputs[sample_idx]
if predicted_label != desired_label:
weights = update_weights(weights,
learning_rate=0.001)
return weights
def predict_outputs(weights, data_inputs, activation="relu"):
predictions = numpy.zeros(shape=(data_inputs.shape[0]))
for sample_idx in range(data_inputs.shape[0]):
r1 = data_inputs[sample_idx, :]
for curr_weights in weights:
r1 = numpy.matmul(a=r1, b=curr_weights)
if activation == "relu":
r1 = relu(r1)
elif activation == "sigmoid":
r1 = sigmoid(r1)
predicted_label = numpy.where(r1 == numpy.max(r1))[0][0]
predictions[sample_idx] = predicted_label
return predictions
f = open("dataset_features.pkl", "rb")
data_inputs2 = pickle.load(f)
f.close()
features_STDs = numpy.std(a=data_inputs2, axis=0)
data_inputs = data_inputs2[:, features_STDs > 50]
f = open("outputs.pkl", "rb")
data_outputs = pickle.load(f)
f.close()
HL1_neurons = 150
input_HL1_weights=numpy.random.uniform(low=-0.1, high=0.1,
size=(data_inputs.shape[1], HL1_neurons))
HL2_neurons = 60
HL1_HL2_weights=numpy.random.uniform(low=-0.1, high=0.1,
size=(HL1_neurons, HL2_neurons))
output_neurons = 4
HL2_output_weights=numpy.random.uniform(low=-0.1, high=0.1,
size=(HL2_neurons, output_neurons))
weights = numpy.array([input_HL1_weights,
HL1_HL2_weights,
HL2_output_weights])
weights = train_network(num_iterations=10,
weights=weights,
data_inputs=data_inputs,
data_outputs=data_outputs,
learning_rate=0.01,
activation="relu")
predictions = predict_outputs(weights, data_inputs)
num_flase = numpy.where(predictions != data_outputs)[0]
print("num_flase ", num_flase.size
“权重”变量包含整个网络的所有权重。基于每个权重矩阵的大小,可以实现动态构建网络结构。例如,如果“input_HL1_weights”变量的大小是102x80,那么我们可以推断出第一个隐藏层有80个神经元。
“train_network”是整个过程的核心功能,因为它通过循环遍历所有样本来构建网络。每个样本都要经过迭代、特征提取、输出标签、权重矩阵乘法、学习速率和激活函数等过程。激活函数有两种选择,ReLU或者sigmoid。ReLU是一个阈值函数,只要它大于零,就会返回相同的输入。否则,ReLU归零。
如果网络对给定样本做出错误预测,则使用“update_weights”函数更新权重。根据学习速率更新权重,准确率不超过45%。为了更高的准确性,可以使用优化算法来更新权重。例如,可以在scikit-learn库的ANN操作中找到梯度下降技术。
原文网址:https://www.kdnuggets.com/2019/02/artificial-neural-network-implementation-using-numpy-and-image-classification.html
学术头条已建立微信交流群,想进群的同学请加学术君微信:AMiner308,记得备注:名字+单位/学校噢。
分享干货