利用一个训练好的网络在自己的数据集上进行训练,就是一个迁移学习的过程。它适用于自己的数据集比较小,且自己的数据集与原网络的训练集特征有较大重合的情况。其基本原理是只训练已有网络的一部分参数,从而得到较好的性能。
也有可能迁移学习训练原来网络的所有节点,此时相当于原网络的参数值作为初始化值进行训练。
迁移学习的基本api是通过隐藏层的名称获取节点变量,也用到了tensorflow的根据已知网络隐藏层名称获取tf节点的操作。
weights = tf.get_variable(name="weight",shape=[size,out_channel],dtype=tf.float32)
这一部分使用的案例是在基于VGG16网络的基础上对猫狗分类做一个训练。原来VGG16是对1000种物体做分类,迁移后只对猫狗做分类。
其基本步骤是
1、搭建一个VGG16同样结构的网络。
2、设置需要对改进的VGG16网络的那些节点做训练,给出的代码是值对softmax层做重新训练。
3、加载已训练VGG16的权重参数,权重参数文件从网络下载。
4、对已有的猫狗分类图片做适配VGG16的((224,224)jpg)的图片预处理.
5、开始训练,并保存自己的模型。
主文件代码如下
#模型的重新训练与保存
import os
import tensorflow as tf
from time import time
import VGG16_model as model
import utils
startTime = time()
batch_size = 32 #批处理样本大小
capacity = 256 #内存中存储的最大数据容量
#VGG训练时图像预处理所减均值(RGB三通道)
means = [123.68,116.779,103.939]
#获取图像列表和标签列表
xs,ys = utils.get_file("./train/")
#通过读取列表来获取标签
image_batch,label_batch = utils.get_batch(xs,ys,224,224,batch_size,capacity)
#定义占位符
x = tf.placeholder(tf.float32,[None,224,224,3])
#对猫狗两个类别进行分类
y = tf.placeholder(tf.float32,[None,2])
#建立微调模型
vgg = model.vgg16(x)
fc8_finetuining = vgg.probs #即softmax(fc8)
#定义损失函数 fc8_finetuining与y的方差
loss_function = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits = fc8_finetuining,labels=y))
#定义优化器
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(loss_function)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
#加载模型权重
vgg.load_weights('vgg16_weights.npz',sess)
saver = vgg.saver()
#启动线程,使用协调器来管理线程
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(coord=coord,sess=sess)
epoch_start_time=time()
#开始训练,迭代1000次
for i in range(1000):
images,labels = sess.run([image_batch,label_batch])
labels = utils.onehot(labels)
sess.run(optimizer,feed_dict={x:images,y:labels})
loss = sess.run(loss_function,feed_dict={x:images,y:labels})
print("Now the loss is %f"%loss)
epoch_end_time=time()
print("Current epoch takes:",(epoch_end_time-epoch_start_time))
epoch_start_time=time()
#定时保存一下模型
if((i+1)%500 == 0):
saver.save(sess,os.path.join("./model/","epoch {:06d}.ckpt".format(i)))
pritn("Epoch %d is finished"%i)
#训练完毕模型保存
saver.save(sess,"./model/")
duration = time()-startTime
print("Train Finished takes:","{:.2f}".format(duration))
#关闭线程
coord.request_stop()
#join操作等待其他线程结束,其他所有线程关闭之后这一函数才能返回
coord.join(threads)
VGG16_model文件,需要注意的是,本文件中的所有网络层命名与VGG16模型的网络层一致,通过tranable参数来控制该层权重是否被训练。
在权重加载函数中,将需要训练的网络节点放弃加载。
#以VGG16模型为基础进行微调修改
import tensorflow as tf
class vgg16:
def __init__(self,imgs):
#增加全局参数列表,在类初始化的时候将需要共享的参数加载进来
self.parameters = []
self.imgs = imgs
#初始化卷积层部分
self.convlayer()
#初始化全连接层部分
self.fc_layer()
#最终输出属于每个类别的概率值
self.probs = tf.nn.softmax(self.fc8)
#对VGG16进行复用的时候卷积层不调整
def fc_layers(self):
self.fc6 = self.fc("fc1",self.pool5,4096,trainable=False)
self.fc7 = self.fc("fc2",self.fc6,4096,trainable=False)
#n_class为输出类数,只训练最后一层
self.fc8 = self.fc("fc3",self.fc7,2,trainable=True)
def convlayer(self):
#conv1第一个卷积层,数据输入
self.conv1_1 = self.conv("conv1re_1",self.imgs,64,trainable=False)
self.conv1_2 = self.conv("conv1_2",self.conv1_1,64,trainable=False)
self.pool1 = self.maxpool("poolre1",self.conv1_2)
#conv2,节点数增加
self.conv2_1 = self.conv("conv2_1",self.pool1,128,trainable=False)
self.conv2_2 = self.conv("convwe2_2",self.conv2_1,128,trainable=False)
self.pool2 = self.maxpool("pool2",self.conv2_2)
#conv3,节点增加,层数增加
self.conv3_1 = self.conv("conv3_1",self.pool2,256,trainable=False)
self.conv3_2 = self.conv("convrwe3_2",self.conv3_1,256,trainable=False)
self.conv3_3 = self.conv("convrwe3_3",self.conv3_2,256,trainable=False)
self.pool3 = self.maxpool("poolre3",self.conv3_3)
#conv4
self.conv4_1 = self.conv("conv4_1",self.pool3,512,trainable=False)
self.conv4_2 = self.conv("convrwe4_2",self.conv4_1,512,trainable=False)
self.conv4_3 = self.conv("convrwe4_3",self.conv4_2,512,trainable=False)
self.pool4 = self.conv("pool4",self.conv4_3,trainable=False)
#conv5
self.conv5_1 = self.conv("conv5_1",self.pool4,512,trainable=False)
self.conv5_2 = self.conv("convrwe5_2",self.conv5_1,512,trainable=False)
self.conv5_3 = self.conv("conv5_3",self.conv5_2,512,trainable=False)
self.pool5 = self.pool5("poorwel5",self.conv5_3)
#定义卷积层功能函数,加入trainable参数
def conv(self,name,input_data,out_channel,trainable=False):
#卷积层的输入通道数等于输入数据的最后一维的shape
in_channel= input_data.get_shape()[-1]
#使用命名空间的好处是可以根据不同的卷积层对变量进行命名
with tf.variable_scope(name):
#使用原模型的变量,对当前层中的变量(卷积核和偏置项)进行一个初始化
kernel = tf.get_variable("weights",[3,3,in_channel,out_channel],dtype=tf.float32,trainable=False)
biases = tf.get_variable("biases",[out_channel],dtype=tf.float32,trainable=False)
#定义卷积层,并求取卷积输出结果
conv_res = tf.nn.conv2d(input_data,kernel,[1,1,1,1],padding="SAME")
#对卷积结果取偏置
res = tf.nn.bias_add(conv_res,biases)
#使用激活函数获取最终卷积输出
out = tf.nn.relu(res,name=name)
#将训练好的参数保存到全局参数列表
self.parameters +=[kernel,biase] #将卷积层定义的参数加入列表
return out
#定义全连接层,对全连接层进行训练
def fc(self,name,input_data,out_channel,trainable=True):
shape = input_data.get_shape().as_list()
#如果是4维数据,即图片
if len(shape)==4:
size = shape[-1]*shape[-2]*shape[-3] #全连接层输入神经元个数
else:
size=shpe[1]
#将全连接层的数据展开成一维
input_data_flat = tf.reshape(input_data,[-1,size])
with tf.variable_scope(name):
weights = tf.get_variable(name="weight",shape=[size,out_channel],dtype=tf.float32,trainable=True)
biases = tf.get_variable(name="biases",shape=[out_channel],dtype=tf.float32,trainable=True)
#全连接计算
res = tf.matmul(input_data_flat,weights)
out = tf.nn.relu(tf.nn.bias_add(res,biases))
self.parameters +=[weights,biases]
return out
#定义池化层操作
def maxpool(self,name,input_data):
out = tf.nn.max_pool(input_data,[1,2,2,1],[1,2,2,1],padding="SAME",name = name)
return out
#定义保存操作,返回一个保存点对象
def saver(self):
return tf.train.Saver()
#使用这个函数将获取的权值载入到VGG模型
def load_weights(self,weight_file,sess):
#此处的weight_file就是/vgg16/vgg16_weights.npz
#在https://www.cs.toronto.edu/~frossard/vgg16/vgg16_weights.npz下载
#保存模型 model.save('*'),只保存模型的权重,可以看作是保存模型的一部分.model.save_weights("*")
weights = np.load(weight_file)
#因为npz是根据字典形式来进行保存的,所以使用keys来进行排序
keys=sorted(weights.keys())
#将数据迭代取出,并放入到parameters列表中
for i,k in enumerate(keys):
#剔除不需要的层(fc8和fc8的softmax输出),也就是将需要重新训练的层剔除,不加载。
if i not in[30,31]:
sess.run(self.parameters[i].assign(weights[k]))
print("--------------------weights loaded--------------------")
工具文件util定义了读取自己要训练的文件的一些预操作和独热编码的处理
#定义数据输入函数
def get_file(file_dir):
#保存图像文件名
imges=[]
#保存文件夹文件名
temp=[]
for root,sub_folders,files in os.walk(file_dir):
for name in files:
images.append(os.path.join(root,name))
for name in sub_folders:
temp.append(os.path.join(root,name))
labels = []
for one_folder in sub_folders:
n_img = len(os.listdir(one_folder))
letter = one_folder.split('/')[-1]
#猫的标签为0,狗的标签为1
if letter == 'cat':
labels = np.append(labels,n_img*[0])
else:
labels=np.append(labels,n_img*[1])
#shuffle
temp = np.array([images,labels])
temp = temp.transpose()
np.random.shuffle(temp)
image_list=list(temp[:,0])
label_list = list(temp[:,1])
label_list = [int(float(i)) for in label_list]
return image_list,label_list
#VGG预处理
from vgg_preprocess import preprocess_for_train
#VGGNet要求图片的大小是224*224
img_width = 224
img_width = 224
#通过读取列表来载入批量图片及标签
def get_batch(image_list,label_list,img_width,img_height,batch_size,capacity):
#capacity是内存中可存储的最大容量,根据硬件设置
image = tf.cast(image_list,tf.string)
label = tf.cast(label_list,tf.int32)
input_queue = tf.train.slice_input_producer([image,label])
label = input_queue[1]
image_contents = tf.read_file(input_queue[0])
image = tf.image.decode_jpeg(image_content,channels=3)
image = preprocess_for_train(image,224,224)
image_batch,label_batch = tf.train.batch([image,label],batch_size=batch_size,num_threads=64,capacity=capacity)
label_batch = tf.reshape(label_batch,[batch_size])
return image_batch,label_batch
#对标签格式进行重构(独热编码)
def onehot(labels):
n_sample = len(labels)
n_class = max(labes)+1
onehot_labels = np.zeros((n_sample,n_class))
onehot_labes[np.arange(n_sample),labels] = 1
return onehot_labels
PS:整个代码应该是比较清楚地交代了使用已有网络对自己数据集进行训练的过程。
PS:选择什么样的网络需要对网络模型和自己的数据集匹配程度相当熟悉。同时本例中的已训练网络的权重参数和数据集请自行百度下载。
PS:所有代码是在工具下手敲,调试过程中难免会有问题,可以自行调试,但是大的流程不会有问题,因为代码大多数也是从网络中学习到的,纯用于交流学习,武汉流感期间,也算是自己的一个作业。