给定文本数据training.txt。
每一行格式为:{"label": "label_name", "content": "content_n"}
类别标签有四个。
通过大量其他的新闻文本训练一个word2vec模型,將赛题数据的word2vec向量作为cnn的输入。
前期数据处理:
训练word2vec的新闻数据使用的是搜狐实验室下的新闻数据(四个文件:”news_sohusite_xml.txt”,”news_tensite_xml.txt”, “sougou2008.txt”, “sougoucs2008.txt”)。读取数据后使用HanLP分词,得到Hanlp_cut_data.txt文件。
安装下载word2vec,进入文件所在目录,使用命令:
./word2vec -train Hanlp_cut_data.txt -output Hanlp_cut_news.bin -cbow 0 -size 200 -window 5 -negative 0 -hs 1 -sample 1e-3 -threads 12 -binary 1
cbow: 1表示使用cbow。默认为skip-Gram。
size:每个词的向量维度,这里我取200,在本文后面的大部分地方我写为embedding_size,一个意思。
window:训练窗口,考虑一个词前后window个词语。
sample:采样阈值,如果一个词语在训练样本中出现的频率越大,越会被采样。
negative 0 -hs 1:表示不使用NEG方法,使用HS方法。
-binary:1表示二进制存储,0表示普通存储,普通存储打开是可以看到词语和向量的
由此得到word2vec的词向量模型:Hanlp_cut_news.bin。
接下来就是如何把训练数据 training.txt 转变成输入向量。
首先我们把训练集中所有词语映射到一个表vocab中,这个表包含词语与下标。
vocab: {'UNKNOW': 0, 'w1': 1, 'w2': 2, 'w3': 3, ....'wn': n}
其中默认第一个映射为{‘UNKNOW’: 0},并且在这个过程中去掉了部分低频词。
再构造一个embedding_matrix矩阵,row_i处的向量为vocab[i]词语的word2vec向量。使用随机数初始化矩阵,如果词语在word2vec词典中存在向量则覆盖。因此,对于训练数据而言,其中低频词和word2vec词典中的未收录词其对应向量都是随机向量。
embedding_matrix (n * embedding_size):
[w1_vector
w2_vector
...
wn_vector]
在我们输入cnn之前、处理句子的时候都是使用词语下标而不是直接使用词语向量,在cnn中的时候会使用 lookup 的方法:tf.nn.embedding_lookup(self.word_embedding, self.input_x)
將其转为向量,这样可以避免数据量过大。
同时,统计计算得到训练数据中最长的篇幅为一万多个字,平均字长为四百多,所以我划定每一条长度(max_length)为1000,不足补零,有多舍弃。由此,我们將每一行的content处理成了max_length个vocab[word],如果是第四类标签向量则为:[0,0,0,1].
使用十折交叉检验方法,把数据集划分为十份,每次取其中一份为测试数据。
embedding_matrix和所有的数据我都用pickle.load()的方法存储为二进制文件。
模型训练:
先简要介绍下text_cnn的分类过程
(该算法由YoonKim在04年论文 “Convolutional Neural Networks for Sentence Classification”中提出)。
图片来源于网络
在这个图片中,输入层是max_length个词语×embedding_size个维度,不同过滤器尺寸(filter_size)有三个:2,3,4,每个尺寸的过滤器有两个(num_filters),池化部分采用的方法是最大池化,即取最大数值。合并这些输出并进行softmax处理(softmax就是將输出转为不同类别上的概率分布)
下面是对部分代码的解析:
# 加载embedding_matrix,记得以'rb'二进制文件的形式打开,否则报错
word_embedding_vector = pickle.load(open(path_to_embedding_matrix, 'rb'))
self.input_x = tf.placeholder(tf.int32, [None, max_length], name="input_x")
self.input_y = tf.placeholder(tf.float32, [None, num_classes], name="input_y")
# 设置droupout
self.dropout_keep_prob = tf.placeholder(tf.float32, name="dropout_keep_prob")
# L2正则
l2_loss = tf.constant(0.0)
with tf.name_scope("embedding"):
self.word_embedding = tf.Variable(tf.to_float(word_embedding_vector))
# lookup转为词语对应向量
self.embedded_chars = tf.nn.embedding_lookup(self.word_embedding, self.input_x)
# 增加一个维度,以匹配输入input_x
self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)
在训练过程中使用droupout,随机地让某些节点不工作,可以提高模型的可靠性和避免过拟合。
pooled_outputs = []
for i, filter_size in enumerate(filter_sizes):
with tf.name_scope("conv_maxpool-%s" % filter_size):
filter_shape = [filter_size, embedding_size, 1, num_filters]
# 卷积层权重w
w = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="conv-W")
# 偏置b
b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name="conv-b")
# 卷积操作 convolutional
conv = tf.nn.conv2d(
self.embedded_chars_expanded, # 当前层节点矩阵
w, # W 是卷积矩阵权重
strides= [1, 1, 1, 1], # 不同维度上的步长
padding="VALID", # 表示卷积核不在边缘做填补,也就是“窄卷积”
name="conv"
)
# 池化操作 pool
# h 是经过非线性激活函数的输出
h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")
pooled = tf.nn.max_pool(
h,
ksize= [1, max_length - filter_size + 1, 1, 1], # 过滤器尺寸
strides=[1, 1, 1, 1], # 步长信息
padding='VALID', # 是否零填充
name="pool"
)
# pooled_outputs保存每次的卷积结果
pooled_outputs.append(pooled)
num_filters_total = num_filters * len(filter_sizes)
self.h_pool = tf.concat(3, pooled_outputs)
self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])
卷积操作和池化操作都有一个步长信息:strides[batch, height, width, channcels]
batch:batch_size样本数目的多少
height:单个样本的行数
width:列数
channcels:通道数目
二维操作只作用在height和width上,因此,一定有:strides[0] = strides[3]=1
padding = ‘SAME’表示全零填充,’VALID’表示不填充
tf.concat()在第三维上合并数据,reshape列表中的-1表示这一维度不用我们自己来指定,最后的h_pool_flat將会是 len(input_x) * num_filters_total。
下面是我自己抽象的理解……
with tf.name_scope("dropout"):
self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)
with tf.name_scope("output"):
w = tf.get_variable(
"output-w",
shape = [num_filters_total, num_classes],
initializer= tf.contrib.layers.xavier_initializer()
)
b = tf.Variable(tf.constant(0., shape=[num_classes]), name= "output-b")
l2_loss += tf.nn.l2_loss(w)
l2_loss += tf.nn.l2_loss(b)
# 各个类别的概率分布
self.scores = tf.nn.softmax(tf.nn.xw_plus_b(self.h_drop, w, b, name="scores"))
# 取概率最大的为输出类别
self.predictions = tf.argmax(self.scores, 1, name="predictions")
# 计算损失函数
with tf.name_scope("loss"):
self.prob = tf.nn.softmax(self.scores)
# softmax_cross_entropy_with_logits 带softmax的交叉熵计算
losses = tf.nn.softmax_cross_entropy_with_logits(logits=self.prob, labels=self.input_y)
# reduce_mean取均方差,后部分为L2正则,lambda是正则化的权重,l2_loss是需要正则的参数
self.loss = tf.reduce_mean(losses) + l2_reg_lambda * l2_loss
# 计算准确率
with tf.name_scope("accuracy"):
correct_predictions = tf.equal(self.predictions, tf.argmax(self.input_y, 1))
self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float"), name="accuracy")
模型的保存用saver.save
(train_cnn.py)
加载使用训练好的模型:
checkpoint_file = tf.train.latest_checkpoint(checkpoint_dir)
graph = tf.Graph()
With graph.as_default():
session_conf = tf.ConfigProto(
allow_soft_placement=FLAGS.allow_soft_placement,
log_device_placement=FLAGS.log_device_placement
)
sess = tf.Session(config=session_conf)
with sess.as_default():
saver = tf.train.import_meta_graph()