本次我的分享内容如标题所述。主要分为以下三个部分:
- 原始代码分析:主要分析一段用CNN进行图片分类的代码,包括功能介绍、数据集及其读入方式、代码的整体结构和流程。
- 代码迁移:介绍如何将上述代码修改后运用于自己的分类任务。包括一般原则和实例讲解两个部分。
- 学习感悟:这部分总结一下我阅读代码的方法。也期待大家有更好的方法拿出来分享交流。
1.原始代码解读
例子——手势识别
这个例子来自于deeplearning第四课第一周的课程作业,它可以实现对6种不同手势图片的识别。
数据集及其读入方式
这个例子的训练集有1080张,测试集有120张,分别储存在了train_signs.h5和test_signs.h5两个文件里。
关于H5文件的读写操作,可以参考这篇博客:hdf5文件读取和写入。
从两个H5文件中分别读取训练集和测试集的图片和标签,分别保存到四个矩阵中。随后用以下代码检查是否成功读入。
index = 99
plt.imshow(X_train_orig[index])
print ("y = " + str(np.squeeze(Y_train_orig[:, index])))
整体流程
这个例子几乎所有的代码都是写在一个ipynb文件里的,只有两个子函数load_dataset和random_mini_batches是放在一个自定义模块cnn_utils.py里然后用命令from cnn_utils import *实现对这两个函数的调用。这样设计主要是考虑到这是一个作业,这个作业在引导学生把关注点放在CNN的实现过程。所以数据读入和生成随机的minibatch此时就显得没那么重要。但我觉得数在实际工程中,数据读入和预处理应该是一个重点。
读入数据后,依次构建了几个子函数:
- 创建占位符号
- 参数初始化
- 前向传播
- 计算代价函数
我们会发现上述子函数里没有反向传播这一步。实际上反向传播就是参数优化,CNN里的反向传播算法没有作为作业要求。不过这一步骤有现成的函数可以调用,接下来就会讲。这个例子的主函数:
def model(X_train, Y_train, X_test, Y_test, learning_rate = 0.003,
num_epochs = 100, minibatch_size = 16, print_cost = True):
"""
Implements a three-layer ConvNet in Tensorflow:
CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED
Arguments:
X_train -- training set, of shape (None, 64, 64, 3)
Y_train -- test set, of shape (None, n_y = 6)
X_test -- training set, of shape (None, 64, 64, 3)
Y_test -- test set, of shape (None, n_y = 6)
learning_rate -- learning rate of the optimization
num_epochs -- number of epochs of the optimization loop
minibatch_size -- size of a minibatch
print_cost -- True to print the cost every 100 epochs
Returns:
train_accuracy -- real number, accuracy on the train set (X_train)
test_accuracy -- real number, testing accuracy on the test set (X_test)
parameters -- parameters learnt by the model. They can then be used to predict.
"""
ops.reset_default_graph() # to be able to rerun the model without overwriting tf variables
seed = 3 # to keep results consistent (numpy seed)
(m, n_H0, n_W0, n_C0) = X_train.shape
n_y = Y_train.shape[1]
costs = [] # To keep track of the cost
X, Y = create_placeholders(n_H0, n_W0, n_C0, n_y) # Create Placeholders of the correct shape
parameters = initialize_parameters() # Initialize parameters
Z3 = forward_propagation(X, parameters) # Forward propagation
cost = compute_cost(Z3, Y) # Cost function
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost) # Backpropagation
init = tf.global_variables_initializer() # Initialize all the variables globally
# Start the session to compute the tensorflow graph
with tf.Session() as sess:
# Run the initialization
sess.run(init)
# Do the training loop
for epoch in range(num_epochs):
minibatch_cost = 0.
num_minibatches = int(m / minibatch_size) # number of minibatches of size minibatch_size in the train set
seed = seed + 1
minibatches = random_mini_batches(X_train, Y_train, minibatch_size, seed)
for minibatch in minibatches:
# Select a minibatch
(minibatch_X, minibatch_Y) = minibatch
# IMPORTANT: The line that runs the graph on a minibatch.
# Run the session to execute the optimizer and the cost, the feedict should contain a minibatch for (X,Y).
_ , temp_cost = sess.run([optimizer, cost], feed_dict={X:minibatch_X, Y:minibatch_Y})
minibatch_cost += temp_cost / num_minibatches
# Print the cost every epoch
if print_cost == True and epoch % 5 == 0:
print ("Cost after epoch %i: %f" % (epoch, minibatch_cost))
if print_cost == True and epoch % 1 == 0:
costs.append(minibatch_cost)
# plot the cost
plt.plot(np.squeeze(costs))
plt.ylabel('cost')
plt.xlabel('iterations (per tens)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()
# Calculate the correct predictions
predict_op = tf.argmax(Z3, 1)
correct_prediction = tf.equal(predict_op, tf.argmax(Y, 1))
# Calculate accuracy on the test set
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
print(accuracy)
train_accuracy = accuracy.eval({X: X_train, Y: Y_train})
test_accuracy = accuracy.eval({X: X_test, Y: Y_test})
print("Train Accuracy:", train_accuracy)
print("Test Accuracy:", test_accuracy)
return train_accuracy, test_accuracy, parameters
这段代码可以分成五块。
def model(X_train, Y_train, X_test, Y_test, learning_rate = 0.003,
num_epochs = 100, minibatch_size = 16, print_cost = True):
第一块是定义主函数的输入参数。运行主函数的时候可以只传入前四个参数,后面的参数直接在def这一行修改。
ops.reset_default_graph() # to be able to rerun the model without overwriting tf variables
tf.set_random_seed(1) # to keep results consistent (tensorflow seed)
seed = 3 # to keep results consistent (numpy seed)
(m, n_H0, n_W0, n_C0) = X_train.shape
n_y = Y_train.shape[1]
costs = []
第二块是初始化和参数获取。一开始是ops.reset_default_graph(),看了网上的资料,我的理解就是把tensorflow的计算图重置,以保证内存中没有其他的Graph,类似于把整个缓存空间清空,重启,不让计算受到之前的计算的影响。 (m, n_H0, n_W0, n_C0) = X_train.shape和 n_y = Y_train.shape[1]是为了获取训练集的尺寸参数,并据此创建 X和Y的placeholders。
X, Y = create_placeholders(n_H0, n_W0, n_C0, n_y) # Create Placeholders of the correct shape
parameters = initialize_parameters() # Initialize parameters
Z3 = forward_propagation(X, parameters) # Forward propagation
cost = compute_cost(Z3, Y) # Cost function
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost) # Backpropagation
init = tf.global_variables_initializer() # Initialize all the variables globally
第三块是创建整个tensorflow的计算图。这一块其实已经把CNN的框架给搭出来了,只是还没有开始真正计算。这一部分就把前面构建的四个子函数依次用上了,而tf.train.AdamOptimizer调用了tf自带的优化算法,这一步事实上就是CNN的反向传播的过程。最后通过tf.global_variables_initializer()初始化变量。
# Start the session to compute the tensorflow graph
with tf.Session() as sess:
# Run the initialization
sess.run(init)
# Do the training loop
for epoch in range(num_epochs):
minibatch_cost = 0.
num_minibatches = int(m / minibatch_size) # number of minibatches of size minibatch_size in the train set
seed = seed + 1
minibatches = random_mini_batches(X_train, Y_train, minibatch_size, seed)
for minibatch in minibatches:
# Select a minibatch
(minibatch_X, minibatch_Y) = minibatch
# IMPORTANT: The line that runs the graph on a minibatch.
# Run the session to execute the optimizer and the cost, the feedict should contain a minibatch for (X,Y).
_ , temp_cost = sess.run([optimizer, cost], feed_dict={X:minibatch_X, Y:minibatch_Y})
minibatch_cost += temp_cost / num_minibatches
# Print the cost every epoch
if print_cost == True and epoch % 5 == 0:
print ("Cost after epoch %i: %f" % (epoch, minibatch_cost))
if print_cost == True and epoch % 1 == 0:
costs.append(minibatch_cost)
第四块是训练。先初始化然后开启一个tf.Session()对话,在这个对话里面有两层循环,第一层循环代表每一次的迭代,第二层代表每次迭代中的一个minibatch。这里面最核心的一句代码是
_ , temp_cost = sess.run([optimizer, cost], feed_dict={X:minibatch_X, Y:minibatch_Y})
相当于把具体的数据带入之前的模型计算,输出的是某一次迭代中的某一个minibatch所产生的代价函数最优值。
# plot the cost
plt.plot(np.squeeze(costs))
plt.ylabel('cost')
plt.xlabel('iterations (per tens)')
plt.title("Learning rate =" + str(learning_rate))
plt.show()
# Calculate the correct predictions
predict_op = tf.argmax(Z3, 1)
correct_prediction = tf.equal(predict_op, tf.argmax(Y, 1))
# Calculate accuracy on the test set
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
print(accuracy)
train_accuracy = accuracy.eval({X: X_train, Y: Y_train})
test_accuracy = accuracy.eval({X: X_test, Y: Y_test})
print("Train Accuracy:", train_accuracy)
print("Test Accuracy:", test_accuracy)
第五块是输出代价函数曲线以及计算模型在测试集上的准确率。
2.代码迁移
这部分的内容就是将如何将手势识别的代码修改并迁移到自己的图片分类任务上。这部分首先讲代码迁移中要注意的几个关键点,然后具体讲讲代码迁移的两个应用。
代码迁移需要注意的几点
在代码迁移过程中,我认为要注意三个方面:数据读取、参数修改、输出检验。
- 数据读取是很重要的一步。不同的数据格式应采取不同的读取方式。在读取过程中,输入一定要和原始代码做好衔接。要么在读取图片后对图片进行预处理,使之与原始代码的输入格式相匹配;要么调整原始代码,使之适应图片的格式。
- 关于参数修改。要把别人的代码拿来自己用,其实不一定要把所有地方都看懂(前提是这段代码能用)。要改的地方重点看,其他地方只要大致知道它实现的功能以及输入输出的格式是什么就行了。
- 关键地方最好能输出一下结果。比如将数据导入矩阵后,把矩阵中的某一个元素输出出来,看有没有问题。
代码迁移应用——基于偏振图像的海藻分类
我用上面的代码做了两个分类任务,第一个任务之前在群里说过,就是用来区分猫、狗、猪三种动物。当时不知道用tensorflow里面有读取图片的函数,就用opencv里面的函数读的,所以当时就感觉图片读得有些慢,后来在跑猫狗大战的程序时发现了tensorflow里面有读取图片的函数。下面就是猫狗大战的程序里读取图片的代码。
# 将python.list类型转换成tf能够识别的格式
image = tf.cast(image, tf.string)
label = tf.cast(label, tf.int32)
# 生成队列
input_queue = tf.train.slice_input_producer([image, label])
image_contents = tf.read_file(input_queue[0])
label = input_queue[1]
image = tf.image.decode_jpeg(image_contents, channels=3)
如果图片是jpg格式的,可以用上面的代码来导入。在此之前先把图片保存在指定文件夹里,然后获取每张图片的路径及标签,分别保存到image和label两个list里面。
我做的第二个任务就是接下来要介绍的海藻分类。这个来源于我室友在做的一个课题的一部分。现在只是先用一小部分数据做初步尝试,看看分类效果如何,如果效果好的话再接着做下去。
先简单介绍下这个数据集。首先什么是偏振图像。简单来说,如果要表征一个图像,普通的彩色图像是3通道,而偏振图像是16通道,所以偏振图像肯定能包含一些普通图像所不具备的信息;然后,需要分类的海藻类别数为3,训练集每类海藻有300张图,测试集每类海藻有70张图,每幅图像均以h5文件的形式储存。
讲一下h5文件的读取。一开始室友把数据给我的时候,我只知道一个h5文件代表了一个海藻样本的16张偏振图,不知道里面的具体结构。于是我就选了一个h5文件分析了一下:
f = h5py.File('train_h5f/1/10003.h5', "r")
for each in f.keys():
print(each) #查看所有的主键
输出结果为DS2。说明数据全部存储在DS2这个key里。那就再把里面的数据导出来看看:
a = f['DS2'][:]
f.close()
a.shape
输出为(16, 32, 32),说明a中第1维为通道数,第2、3维为图片的长和宽。验证一下:
plt.imshow(a[1])
整个数据集的读取的代码参考了这篇博客:如何用TensorFlow训练和识别/分类自定义图片。
# 第一次遍历图片目录是为了获取图片总数
input_count = 0
for i in range(0,3):
dir = '/home/lyq/桌面/train_h5f/%s/' % i # 这里应跟据具体情况更改路径
for rt, dirs, files in os.walk(dir):
for filename in files:
input_count += 1
input_count_test = 0
for i in range(0,3):
dir = '/home/lyq/桌面/test_h5f/%s/' % i # 这里应跟据具体情况更改路径
for rt, dirs, files in os.walk(dir):
for filename in files:
input_count_test += 1
print(input_count,input_count_test)
# 定义对应维数和各维长度的数组
X_train = np.zeros([input_count,a.shape[1],a.shape[2],16], np.float32) #对于偏振图片,通道数为16;把uint8改成了uint32
Y_train = np.array([[0]*3 for i in range(input_count)]) #分3类,故向量长度为3
X_test = np.zeros([input_count_test,a.shape[1],a.shape[2],16], np.float32) #类似于X_train
Y_test = np.array([[0]*3 for i in range(input_count_test)]) #类似于Y_train
X_train.shape,Y_train.shape,X_test.shape,Y_test.shape
# 第二次遍历图片目录是为了生成图片数据和标签
index = 0
for i in range(0,3):
dir = '/home/lyq/桌面/train_h5f/%s/' % i # 这里应跟据具体情况更改路径
for rt, dirs, files in os.walk(dir):
for filename in files:
filename = dir + filename
f = h5py.File(filename, "r")
a = f['DS2'][:]
f.close()
X_train[index] = a.transpose(1,2,0)
Y_train[index][i] = 1
index += 1
index_test = 0
for i in range(0,3):
dir = '/home/lyq/桌面/test_h5f/%s/' % i # 这里应跟据具体情况更改路径
for rt, dirs, files in os.walk(dir):
for filename in files:
filename = dir + filename
f = h5py.File(filename, "r")
a = f['DS2'][:]
f.close()
X_test[index_test] = a.transpose(1,2,0)
Y_test[index_test][i] = 1
index_test += 1
print(X_train.shape,Y_train.shape,X_test.shape,Y_test.shape)
print(filename)
读取完后验证一下图片是否正确读入到矩阵中:
# Example of a picture
index = 8
plt.imshow(X_train[index,:,:,2])
print ("y = " + str(np.squeeze(Y_train[index,:])))
之后需要修改参数的地方有两处,一处在forward_propagation里面:
调整ksize和strides的值,并把最后一行里的6改为3(因为一共有3类海藻)。
然后是在主函数的默认输入参数里:
可以调整学习率、迭代步长、minibatch大小等。
3.学习感悟——如何看代码
之前阅读代码的时候,我会遇到这些问题:
- 看懵了,不想看;
- 迷失于细节,忘记了它要干什么;
- 看懂了,但好像并没有什么用。
后来我总结了阅读代码的四类切入点,这样看到一大段代码的时候就不会感到无从下手,一片迷茫:
- 看代码的输入和输出是什么,能够实现什么功能;
- 看代码的整体结构和流程是怎样的;
- 学习代码中的一些自己不熟悉的命令和技巧;
- 思考如何修改代码使之运用于其他任务。
最后一点其实是在前三点基础上的综合。但其实不一定要把原始代码彻底弄明白后才能修改并运用到其他分类任务上。在代码迁移的时候,先去分辨哪些地方需要修改,哪些地方可以照搬。如果觉得有一块地方需要改,那么这块就得仔细看并且理解;而另一块地方可以直接照搬不用改,那么那块地方就只要明白它的输入和输出是什么就可以,细节暂时不用管。
另外,当阅读遇到瓶颈时,可以先把代码用起来,即修改该代码使之运用到其他的分类任务。这样,当分类任务完成后,便可以获得理解这个代码的新的突破口,然后就又可以从新的突破口出发啃这段代码了。