这篇博客主要用来记录一个从不会tensorflow到第一个project(textCNN—中文短文本分类)正式开张的故事,用来与同样刚入门tf的童鞋交流,大神就不必看了:
对上述Github上代码的做一些注释,帮助类似我的初学者理解,代码可能有不同,请与原来的代码对比,下面会陆续贴部分代码,主要用到了data_helpers.py、text_cnn.py、train.py,第三个可以看做是主函数
这部分主要是定义了两个函数:
(1)第一个是数据载入函数
输入样本文档,输出样本data、样本label的list,这个没啥好说的。不过值得注意的是,这里作者是一次性将所有样本塞进去内存了,存在一个隐患问题,样本大了后会爆内存,别问我为什么会知道。解决方案可能稍后会做一些对比尝试或许更新博客,比如对超大大样本随机抽样(有放回和无放回到底哪样最合适待验证)出100个不爆内存的样本,然后按照随机顺序放进模型训练,训练仍采用minibatch。(为什么使用minibatch训练,我直接给个链接http://hp.stuhome.net/index.php/2016/09/20/tensorflow_batch_minibatch/,其实大家应该都知道原委)
这部分代码请根据自己的样本文档特征修改,我的就不贴了,可能大家的样本情况格式都不一样。
(2)第二个函数为一个batch样本生成器
def batch_iter(data, batch_size, num_epochs, shuffle=True):
"""
Generates a batch iterator for a dataset.批量数据batchsize生成器
定义一个函数,输出batch样本,参数为data(包括feature和label),batchsize,epoch
"""
data = np.array(data)#全部数据转化为array
data_size = len(data)
num_batches_per_epoch = int((len(data)-1)/batch_size) + 1#每个epoch有多少个batch,个数
for epoch in range(num_epochs):
# Shuffle the data at each epoch
if shuffle:
shuffle_indices = np.random.permutation(np.arange(data_size))
shuffled_data = data[shuffle_indices]# shuffled_data按照上述乱序得到新的样本
else:
shuffled_data = data
for batch_num in range(num_batches_per_epoch):#开始生成batch
start_index = batch_num * batch_size
end_index = min((batch_num + 1) * batch_size, data_size)#这里主要是最后一个batch可能不足batchsize的处理
yield shuffled_data[start_index:end_index]
#yield,在for循环执行时,每次返回一个batch的data,占用的内存为常数
这里可以看出作者的细心,第一个是end_index = min((batch_num + 1) * batch_size, data_size),考虑最后最后一个batch可能大小不够batchsize了,这在上一篇博客也见到过,第二个是生成器,而不是返回一个包含各个batch样本的list(这一点是否是我多虑了呢,没有仔细观察内存状况对比)
这部分主要是建立了一个text_cnn结构的类
结构比较简单,一个embedding layer+一个convolution layer(Relu)+一个maxpooling层+softmax
主要会引起思考的问题在哪呢,如果你一行行敲代码就会发现:
(1)每个层的参数设置问题
(2)embedding layer和所谓的word2vec是个什么关系
先回答第二个问题,一开始我接触这个project的时候,天真的想,word2vec不就是一个把词的one-hot形式转化为稠密的短向量表示的工具么,用过后一个文本就变成了稠密矩阵,再当图像处理不行么?当然不行!
为什么?
reason one > 如果你仔细看下word2vec的原理,会发现vector其实是一个中间产物,本质上是模型的一部分参数,那意味着什么,不同的训练目标和样本结构得到的最优参数是不一样的,所以不存在固定的vector来表示某个word,具体分析贴一个链接http://spaces.ac.cn/archives/4122/,这是找了数篇资料才发现的解答,贼棒!当然,上面提到的B站up主也有相关解释,也不错。
reason two > 图像处理的卷积核比如3*3,可以横向和纵向两个方向移动,而文本是不行的,因为一个word的表示就是一个横向量,你不能通过原来这种卷积核把一个单词的表示拆开,破坏单词的表示,在这里,一个单词应该和图像的像素点对应即最小单元(但是实际效果我还没去对比过,后续有时间我可能会做以下对比试验),所以卷积核只会在一个维度上移动,比如词的vector为256长度,那么卷积核应该为3*256。
—另外注意他的maxpooling参数设置,好好感受下,利用CNN解决文本分类问题的文章还是很多的,比如这篇 A Convolutional Neural Network for Modelling Sentences 最有意思的输入是在 pooling 改成 (dynamic) k-max pooling ,pooling阶段保留 k 个最大的信息,保留了全局的序列信息,效果我暂时还没去尝试,后续有兴趣了再更新吧。
然后回答第一个问题:
我当时用torch的时候没有遇到这种问题,因为torch写CNN太轻松了,一个卷积层你直接add一个卷积模块就行了,但是tensorflow很细节化,需要你写出参数矩阵w、b的tensor规格,所以这里涉及卷积核参数维度、移动维度等问题,这里接着贴一个链接,也是找了一些资料才发现回答的比较好的。http://blog.csdn.net/hymanyoung/article/details/65444288直接看“TensorFlow卷积神经网络实践”后面的部分,比较清楚的说明了各参数的结构
这一部分我的整体加注释的代码,可能不专业,但希望有助于理解。
import tensorflow as tf
import numpy as np
class TextCNN(object):#定义了1个TEXTCNN的类,包含一张大的graph
"""
A CNN for text classification.
Uses an embedding layer, followed by a convolutional, max-pooling and softmax layer.
embedding层,卷积层,池化层,softmax层
"""
def __init__(
self, sequence_length, num_classes, vocab_size,
embedding_size, filter_sizes, num_filters, l2_reg_lambda=0.0):#定义各种输入参数,这里的输入是句子各词的索引?
# Placeholders for input, output and dropout
self.input_x = tf.placeholder(tf.int32, [None, sequence_length], name="input_x")
#定义一个operation,名称input_x,利用参数sequence_length,None表示样本数不定,
#不一定是一个batchsize,训练的时候是,验证的时候None不是batchsize
#这是一个placeholder,
#数据类型int32,(样本数*句子长度)的tensor,每个元素为一个单词
self.input_y = tf.placeholder(tf.float32, [None, num_classes], name="input_y")
#这个placeholder的数据输入类型为float,(样本数*类别)的tensor
self.dropout_keep_prob = tf.placeholder(tf.float32, name="dropout_keep_prob")
#placeholder表示图的一个操作或者节点,用来喂数据,进行name命名方便可视化
# Keeping track of l2 regularization loss (optional)
l2_loss = tf.constant(0.0)
#l2正则的初始化,有点像sum=0
#其实softmax是需要的
# Embedding layer
#参见
with tf.device('/cpu:0'), tf.name_scope("embedding"):#封装了一个叫做“embedding'的模块,使用设备cpu,模块里3个operation
self.W = tf.Variable(
tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0),
name="W")#operation1,一个(词典长度*embedsize)tensor,作为W,也就是最后的词向量
self.embedded_chars = tf.nn.embedding_lookup(self.W, self.input_x)
#operation2,input_x的tensor维度为[none,seq_len],那么这个操作的输出为none*seq_len*em_size
self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)
#增加一个维度,变成,batch_size*seq_len*em_size*channel(=1)的4维tensor,符合图像的习惯
# Create a convolution + maxpool layer for each filter size
pooled_outputs = []#空list
for i, filter_size in enumerate(filter_sizes):#比如(0,3),(1,4),(2,5)
with tf.name_scope("conv-maxpool-%s" % filter_size):#循环第一次,建立一个名称为如”conv-ma-3“的模块
# Convolution Layer
filter_shape = [filter_size, embedding_size, 1, num_filters]
#operation1,没名称,卷积核参数,高*宽*通道*卷积个数
W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W")
#operation2,名称”W“,变量维度filter_shape的tensor
b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name="b")
#operation3,名称"b",变量维度卷积核个数的tensor
conv = tf.nn.conv2d(
self.embedded_chars_expanded,
W,
strides=[1, 1, 1, 1],#样本,height,width,channel移动距离
padding="VALID",
name="conv")
#operation4,卷积操作,名称”conv“,与w系数相乘得到一个矩阵
# Apply nonlinearity
h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")
#operation5,加上偏置,进行relu,名称"relu"
# Maxpooling over the outputs
pooled = tf.nn.max_pool(
h,
ksize=[1, sequence_length - filter_size + 1, 1, 1],
strides=[1, 1, 1, 1],
padding='VALID',
name="pool")
pooled_outputs.append(pooled)
#每个卷积核和pool处理一个样本后得到一个值,这里维度如batchsize*1*1*卷积核个数
#三种卷积核,appen3次
# Combine all the pooled features
num_filters_total = num_filters * len(filter_sizes)
#operation,每种卷积核个数与卷积核种类的积
self.h_pool = tf.concat(pooled_outputs, 3)
#operation,将outpus在第4个维度上拼接,如本来是128*1*1*64的结果3个,拼接后为128*1*1*192的tensor
self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])
#operation,结果reshape为128*192的tensor
# Add dropout
with tf.name_scope("dropout"):
self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)
#添加一个"dropout"的模块,里面一个操作,输出为dropout过后的128*192的tensor
# Final (unnormalized) scores and predictions
with tf.name_scope("output"):#添加一个”output“的模块,多个operation
W = tf.get_variable(
"W",
shape=[num_filters_total, num_classes],
initializer=tf.contrib.layers.xavier_initializer())
#operation1,系数tensor,如192*2,192个features分2类,名称为"W",注意这里用的是get_variables
b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name="b")
#operation2,偏置tensor,如2,名称"b"
l2_loss += tf.nn.l2_loss(W)
#operation3,loss上加入w的l2正则
l2_loss += tf.nn.l2_loss(b)
#operation4,loss上加入b的l2正则
self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name="scores")
#operation5,scores计算全连接后的输出,如[0.2,0.7]名称”scores“
self.predictions = tf.argmax(self.scores, 1, name="predictions")
#operations,计算预测值,输出最大值的索引,0或者1,名称”predictions“
# CalculateMean cross-entropy loss
with tf.name_scope("loss"):#定义一个”loss“的模块
losses = tf.nn.softmax_cross_entropy_with_logits(logits=self.scores, labels=self.input_y)
#operation1,定义losses,交叉熵,如果是一个batch,那么是一个长度为batchsize1的tensor?
self.loss = tf.reduce_mean(losses) + l2_reg_lambda * l2_loss
#operation2,计算一个batch的平均交叉熵,加上全连接层参数的正则
# Accuracy
with tf.name_scope("accuracy"):#定义一个名称”accuracy“的模块
correct_predictions = tf.equal(self.predictions, tf.argmax(self.input_y, 1))
#operation1,根据input_y和predictions是否相同,得到一个矩阵batchsize大小的tensor
self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float"), name="accuracy")
#operation2,计算均值即为准确率,名称”accuracy“
就是主函数了,输入样本和超参数,训练网络、存储参数、计算准确率等,带“summary”的函数或者模块是tensorflow储存参数和可视化的工具,我暂时也不怎么熟练,就没怎么写注释,不过不影响功能。
这一节没什么大的引起思考的问题,加了一点点注释和改了一点点参数:
import tensorflow as tf
import numpy as np
import os
import time
import datetime
import data_helpers
from text_cnn import TextCNN
from tensorflow.contrib import learn
# Parameters
# ==================================================
# Data loading params,
#数据集里10%为验证集
tf.flags.DEFINE_float("dev_sample_percentage", .1, "Percentage of the training data to use for validation")
#原数据的文件路径
tf.flags.DEFINE_string("data_file", "/Users/xuhy/Downloads/cnn-text-wujun/data/after_fenci_wujuncnndata", "Data source.")
# Model Hyperparameters
#embedding维度256,4种卷积核,每种128个,0.5的dropout
tf.flags.DEFINE_integer("embedding_dim", 256, "Dimensionality of character embedding (default: 128)")
tf.flags.DEFINE_string("filter_sizes", "2,3,4,5", "Comma-separated filter sizes (default: '2,3,4,5')")
tf.flags.DEFINE_integer("num_filters", 128, "Number of filters per filter size (default: 128)")
tf.flags.DEFINE_float("dropout_keep_prob", 0.5, "Dropout keep probability (default: 0.5)")
tf.flags.DEFINE_float("l2_reg_lambda", 0.0, "L2 regularization lambda (default: 0.0)")
# Training parameters
#batchsize为64,20个epoch,每100个batch后,计算验证集上的表现,每100个batch后保存模型,checkpoint是个啥?
tf.flags.DEFINE_integer("batch_size", 64, "Batch Size (default: 64)")
tf.flags.DEFINE_integer("num_epochs", 20, "Number of training epochs (default: 200)")
tf.flags.DEFINE_integer("evaluate_every", 100, "Evaluate model on dev set after this many steps (default: 100)")
tf.flags.DEFINE_integer("checkpoint_every", 100, "Save model after this many steps (default: 100)")
tf.flags.DEFINE_integer("num_checkpoints", 5, "Number of checkpoints to store (default: 5)")
# Misc Parameters
#true表示自动寻找一个存在并支持的cpu或者gpu,防止指定的设备不存在
#如果将False改为True,可以看到operations被指派到哪个设备运行
tf.flags.DEFINE_boolean("allow_soft_placement", True, "Allow device soft device placement")
tf.flags.DEFINE_boolean("log_device_placement", False, "Log placement of ops on devices")
FLAGS = tf.flags.FLAGS#FLAGS是一个对象,保存了解析后的命令行参数
FLAGS._parse_flags()
print("\nParameters:")
for attr, value in sorted(FLAGS.__flags.items()):
print("{}={}".format(attr.upper(), value))
print("")
# Data Preparation
# ==================================================
# Load data
print("Loading data...")
print("start_time"+"\t\t"+str(datetime.datetime.now().isoformat()))
x_text, y = data_helpers.load_data_and_labels(FLAGS.data_file)
print("end_time"+"\t\t"+str(datetime.datetime.now().isoformat()))
#这里的y是数值,x还是单词序列
#!!!这里一次性载入所有数据,注意考虑内存,大数据的情况下如何载入需要分析
# Build vocabulary
print("生成单词索引,构成样本索引矩阵...")
print("start_time"+"\t\t"+str(datetime.datetime.now().isoformat()))
max_document_length = 298#每一条评价的最多单词数字
vocab_processor = learn.preprocessing.VocabularyProcessor(max_document_length)
#单词转化为在字典中的位置,这是一个操作
x = np.array(list(vocab_processor.fit_transform(x_text)))
y = np.array(y)
print("end_time"+"\t\t"+str(datetime.datetime.now().isoformat()))
#在不够长度的评价最后加0,样本变成了索引数值矩阵,这里的x已经是索引序列了,n*seq_len的tensor
# Randomly shuffle data
print("打乱样本顺序...")
print("start_time"+"\t\t"+str(datetime.datetime.now().isoformat()))
np.random.seed(10)
shuffle_indices = np.random.permutation(np.arange(len(y)))#打乱样本
x_shuffled = x[shuffle_indices]#新的乱序样本
y_shuffled = y[shuffle_indices]#新的乱序label
print("end_time"+"\t\t"+str(datetime.datetime.now().isoformat()))
# Split train/test set
# TODO: This is very crude, should use cross-validation训练集、验证集划分完毕,全部是索引数值
print("生成训练集和验证集...")
print("start_time"+"\t\t"+str(datetime.datetime.now().isoformat()))
dev_sample_index = -1 * int(FLAGS.dev_sample_percentage * float(len(y)))#负数,倒过来数
x_train, x_dev = x_shuffled[:dev_sample_index], x_shuffled[dev_sample_index:]#切片
y_train, y_dev = y_shuffled[:dev_sample_index], y_shuffled[dev_sample_index:]
print("Vocabulary Size: {:d}".format(len(vocab_processor.vocabulary_)))#字典长度
print("Train/Dev split: {:d}/{:d}".format(len(y_train), len(y_dev)))#训练集和验证集长度
print("end_time"+"\t\t"+str(datetime.datetime.now().isoformat()))
# Training
# ==================================================
with tf.Graph().as_default():
session_conf = tf.ConfigProto(
allow_soft_placement=FLAGS.allow_soft_placement,
log_device_placement=FLAGS.log_device_placement)#这个session配置,按照前面的gpu,cpu自动选择
sess = tf.Session(config=session_conf)#建立一个配置如上的会话
with sess.as_default():#在上述session填充内容
cnn = TextCNN(
sequence_length=x_train.shape[1],#[0]是样本维度,样本数量,[1]是单个样本的长度
num_classes=y_train.shape[1],#同理,这里是类别数量
vocab_size=len(vocab_processor.vocabulary_),#字典长度
embedding_size=FLAGS.embedding_dim,
filter_sizes=list(map(int, FLAGS.filter_sizes.split(","))),
num_filters=FLAGS.num_filters,
l2_reg_lambda=FLAGS.l2_reg_lambda) #包含一个CNN
#TextCNN是一个类,输入参数,得到一个CNN结构
# Define Training procedure
global_step = tf.Variable(0, name="global_step", trainable=False)#定义一个变量step
optimizer = tf.train.AdamOptimizer(1e-3)#里面是学习速率,选择优化算法,建立优化器
grads_and_vars = optimizer.compute_gradients(cnn.loss)#选择目标函数,计算梯度;返回的是梯度和变量
#函数minimize() 与compute_gradients()都含有一个参数gate_gradient,用于控制在应用这些梯度时并行化的程度。这里没有?
train_op = optimizer.apply_gradients(grads_and_vars, global_step=global_step)#运用梯度
中间summary环节我就没怎么写,就不贴了,直接帖后面的。
# Initialize all variables
sess.run(tf.global_variables_initializer())#初始化所有变量
#定义了一个函数,输入为1个batch
def train_step(x_batch, y_batch):
"""
A single training step
"""
feed_dict = {
cnn.input_x: x_batch,
cnn.input_y: y_batch,
cnn.dropout_keep_prob: FLAGS.dropout_keep_prob
}
_, step, summaries, loss, accuracy = sess.run(
[train_op, global_step, train_summary_op, cnn.loss, cnn.accuracy],
feed_dict)
#梯度更新(更新模型),步骤加一,存储数据,计算一个batch的损失,计算一个batch的准确率
time_str = datetime.datetime.now().isoformat()#当时时间
print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy))
train_summary_writer.add_summary(summaries, step)
#定义了一个函数,用于验证集,输入为一个batch
def dev_step(x_batch, y_batch, writer=None):
"""
Evaluates model on a dev set
"""
#验证集太大,会爆内存,采用batch的思想进行计算,下面生成多个子验证集
num=20
x_batch=x_batch.tolist()
y_batch=y_batch.tolist()
l=len(y_batch)
l_20=int(l/num)
x_set=[]
y_set=[]
for i in range(num-1):
x_temp=x_batch[i*l_20:(i+1)*l_20]
x_set.append(x_temp)
y_temp=y_batch[i*l_20:(i+1)*l_20]
y_set.append(y_temp)
x_temp=x_batch[(num-1)*l_20:]
x_set.append(x_temp)
y_temp=y_batch[(num-1)*l_20:]
y_set.append(y_temp)
#每个batch验证集计算一下准确率,num个batch再平均
lis_loss=[]
lis_accu=[]
for i in range(num):
feed_dict = {
cnn.input_x: np.array(x_set[i]),
cnn.input_y: np.array(y_set[i]),
cnn.dropout_keep_prob: 1.0
}
step, summaries, loss, accuracy = sess.run(
[global_step, dev_summary_op, cnn.loss, cnn.accuracy],
feed_dict)
lis_loss.append(loss)
lis_accu.append(accuracy)
time_str = datetime.datetime.now().isoformat()
print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy))
print("test_loss and test_acc"+"\t\t"+str(sum(lis_loss)/num)+"\t\t"+str(sum(lis_accu)/num))
if writer:
writer.add_summary(summaries, step)
# Generate batches(生成器),得到一个generator,每一次返回一个batch,没有构成list[batch1,batch2,batch3,...]
batches = data_helpers.batch_iter(
list(zip(x_train, y_train)), FLAGS.batch_size, FLAGS.num_epochs)
#zip将样本与label配对,
# Training loop. For each batch...
for batch in batches:
x_batch, y_batch = zip(*batch)#unzip,将配对的样本,分离出来data和label
train_step(x_batch, y_batch)#训练,输入batch样本,更新模型
current_step = tf.train.global_step(sess, global_step)
if current_step % FLAGS.evaluate_every == 0:#每多少步,算一下验证集效果
print("\nEvaluation:")
dev_step(x_dev, y_dev, writer=dev_summary_writer)#喂的数据为验证集,此时大小不止一个batchsize1的大小
print("")
if current_step % FLAGS.checkpoint_every == 0:#每多少步,保存模型
path = saver.save(sess, checkpoint_prefix, global_step=current_step)
print("Saved model checkpoint to {}\n".format(path))
这里当时遇到了一个问题,就是每次验证集计算的时候,内存就爆了,然后我就采用minibatch的想法改进了以下,没毛病了。
初级版本,后续会尽量优化
目前,23w+样本,类别25+,下面是训练不到2个epoch的情况,测试集准确率86.17%:
test_loss and test_acc 0.475186523795 0.861730447412
Saved model checkpoint to /Users/xuhy/runs/1498792747/checkpoints/model-6800
后面有空更。
谢谢!