文章目录
今天学习了发表在NIPS2015的论文:Character-level Convolutional Networks for Text Classification基于CNN的字符级文本分类。现将模型的思想和论文中的重点知识总结如下:
一、Char-CNN模型结构
在此之前很多基于深度学习的模型都是使用高层面的单位对文本或者语言进行建模,比如单词(统计信息或者n-grams、word2vec等),短语(phrases),句子(sentence)层面,或者对语义和语法结构进行分析,但是本文则提出了更细粒度的——字符层面进行文本分类。这样做的好处是不需要使用预训练好的词向量和语法句法结构等信息。除此之外,字符级还有一个好处就是可以很容易的推广到所有语言。首先看一下论文中的模型结构图:
首先必须清楚的一点是该模型的输入是字符层面的one-hot表示的向量,那么首先需要有字符的编码。
1,字符编码
因为模型的输入是字符的one-hot表示向量,所以先得有字符编码,需要做的就是构建字母表,本文中使用的字母表如下,共有70个字符,外加两个"": 0, “”: 1,所以共72个。文中还提到要反向处理字符编码,即反向读取文本,这样做的好处是最新读入的字符总是在输出开始的地方。:
abcdefghijklmnopqrstuvwxyz0123456789
-,;.!?:’’’/\|_@#$%ˆ&*˜‘+-=<>()[]{}
2,模型卷积-池化层
文中提出了两种规模的神经网络–Large和Small。都由6个卷积层和3个全连接层共9层神经网络组成。这里使用的是1-D卷积神经网络。除此之外,在三个全连接层之间加入两个dropout层以实现模型正则化。其参数配置如下图所示:
卷积层的参数配置:
- Large Feature/Small Feature:卷积核的个数;
- Kernal:卷积核的宽度,就是一次计算的字符个数,高都是字符向量的长度,文中是70。这里宽高有的地方会反过来说,但不影响本质,在计算中别搞混就行;
- Pool:池化层的宽,N/A表示没有池化层;
全连接层的参数配置:
-
Output Units Small:全连接层的输出维度;
-
输出层的输出单元个数没有标明,是因为针对不同任务取决于具体的类别数。
除此之外,在三个全连接层之间加入两个dropout层以实现模型正则化,dropout的概率都为0.5。文中设置
l
0
l_{0}
l0 为1014,作者认为1014个字符已经可以捕获大部分感兴趣的文本。文中作者还给出了卷积的最后一层输出序列的长度:
l
6
l_{6}
l6=(
l
0
l_{0}
l0-96)/27=34,具体计算看一下我自己对模型的理解:
二、使用同义词扩充数据集
论文中还提到对于深度学习模型,采用适当的数据增强技术可以提高模型的泛化能力。比如说图像处理中对图片进行缩放、平移、旋转等操作不会改变图片本身含义;语音识别中对语音的声调、语速、噪声也不会改变其结果。但是在文本处理中,却不能随意挑换字符顺序,因为顺序就代表了语义。对于NLP,最理想的数据增强方法是使用人类复述句子(human rephrases of sentences),但是这比较不现实并且对于大规模语料来说代价昂贵。 一个更自然的选择是使用词语或短语的同义词或同义短语进行替换,从而达到数据增强的目的。但是,为了实现该技术,需要解决两个问题:
- 哪些词应当被替换
- 应该是用哪个同义词来替换该词
文中提出了以一定概率的方式随机进行选择,其中q和p都取0.5。
三、该模型的结论
- char-cnn是基于更小粒度的文本分类模型,由于可以不考虑单词的内在含义、语义和语法的信息,所以可以跨语言的使用;
- 根据论文实验结果,char-cnn更适用于不是那么标准的文本,像用户生成数据(user-generated data)。例如用户评论等,在正规文本如新闻等上面竞争力不大;
- 可以学习表情符号和拼写错误;
- 字符表的构建,是否考虑大小写?根据文中的实验结果,数据量小时,区分大小写效果好些;数据量达到百万级时,不区分时效果要好些;
- 该模型在数据量小时没有优势,但是在数据量很大的时候就表现出了它的竞争力。
数据集大小为数十万的,n-gram TFIDF这样的传统方法仍然是的有效方法,只有在数据集达到数百万的规模之后,采用字符级别的ConvNets更有优势。
四、基于tensorflow的实现
1、数据集及数据处理
接下来我们使用AG’s news新闻分类数据集来进行代码实现。数据结构如下图所示,每一行有三项,第一项是类别,第二项是title,第三项是描述。
我们使用二三项连接起来作为训练数据,论文中提到了设置训练数据的字符长度最大为1014,所以最终每个样本数据会被转化为1014*72的矩阵传入神经网络,新建get_data.py文件,具体代码如下:
import numpy as np
import random
import pickle
import os
class data_load():
def __init__(self):
if not os.path.exists("./data/AG/train_datas"):
# 若不存在该文件,则进行划分train、dev、test数据集和对应的标签
self.train_datas, self.train_labels, self.dev_datas, self.dev_labels, self.test_datas, self.test_labels = self.get_train_dev_test_data()
# 将三类数据集中的数据转换为对应的 charid
char_dict = self.get_char_dict()
self.train_datas = self.convert_data_to_charid(self.train_datas, char_dict)
self.dev_datas = self.convert_data_to_charid(self.dev_datas, char_dict)
self.test_datas = self.convert_data_to_charid(self.test_datas, char_dict)
self.num_classes = len(np.unique(self.train_labels))
print(self.train_datas.shape, self.train_labels.shape)
print(self.dev_datas.shape, self.dev_labels.shape)
print(self.test_datas.shape, self.test_labels.shape)
# 写入文件
pickle.dump(self.train_datas, open('./data/AG/train_datas', 'wb'))
pickle.dump(self.train_labels, open('./data/AG/train_labels', 'wb'))
pickle.dump(self.dev_datas, open('./data/AG/dev_datas', 'wb'))
pickle.dump(self.dev_labels, open('./data/AG/dev_labels', 'wb'))
pickle.dump(self.test_datas, open('./data/AG/test_datas', 'wb'))
pickle.dump(self.test_labels, open('./data/AG/test_labels', 'wb'))
else:
# 若存在该文件,则将文件载入
self.train_datas = pickle.load(open('./data/AG/train_datas', 'rb'))
self.train_labels = pickle.load(open('./data/AG/train_labels', 'rb'))
self.dev_datas = pickle.load(open('./data/AG/dev_datas', 'rb'))
self.dev_labels = pickle.load(open('./data/AG/dev_labels', 'rb'))
self.test_datas = pickle.load(open('./data/AG/test_datas', 'rb'))
self.test_labels = pickle.load(open('./data/AG/test_labels', 'rb'))
self.num_classes = len(np.unique(self.train_labels))
print(self.train_datas.shape, self.train_labels.shape)
print(self.dev_datas.shape, self.dev_labels.shape)
print(self.test_datas.shape, self.test_labels.shape)
def read_file(self, path):
datas = open(path, "r", encoding="utf-8").read().splitlines()
labels = [] # 存放类别标签
texts = [] # 存放文本内容
for i, data in enumerate(datas):
data = data.lower() # 数据转换为小写
if i%10000 == 0:
print(i, len(datas))
data = data.split(',"', 1) # 一行数据在第一次出现 ," 的位置分隔
labels.append(int(data[0].strip("\""))-1) # 将首尾的 " 去除,得到标签数字
texts.append(data[1])
return texts, labels
def get_train_dev_test_data(self):
datas, labels = self.read_file("./data/AG/train.csv")
n = int(len(datas)*0.8) # 80%的数据作为训练数据,20%的数据作为dev数据
# 将数据和对应的标签洗牌
cc = list(zip(datas, labels))
random.shuffle(cc)
datas[:], labels[:] = zip(*cc)
train_datas = datas[0:n]
train_labels = labels[0:n]
train_labels = np.array(train_labels)
dev_datas = datas[n:]
dev_labels = datas[n:]
dev_labels = np.array(dev_labels)
test_datas, test_labels = self.read_file("./data/AG/test.csv")
test_labels = np.array(test_labels)
return train_datas, train_labels, dev_datas, dev_labels, test_datas, test_labels
def get_char_dict(self):
chars = '''abcdefghijklmnopqrstuvwxyz
0123456789
-,;.!?:'"/\|_@#$%ˆ&*˜‘
+-=<>()[]{}'''
char_dict = {"<pad>": 0, "<unk>": 1}
for char in chars:
char_dict[char] = len(char_dict)
return char_dict
def convert_data_to_charid(self, datas, char_dict):
new_datas = []
for i, data in enumerate(datas):
if i%10000 == 0:
print(i, len(datas))
new_datas.append([]) # 添加一个空list,用于存放当前行的 charid
for j, char in enumerate(data):
if j==1014: # 句子长度等于1014时截断
break
new_datas[i].append(char_dict.get(char, 1)) # 获得字母的id,添加到当前行所在list中
new_datas[i] = new_datas[i]+[0]*(1014-len(new_datas[i])) # 句子长度不足1014的,要进行补0
new_datas = np.array(new_datas)
return new_datas
if __name__ == '__main__':
data_load = data_load()
2、模型构建
接下来是模型的搭建,该模型的输入是one-hot表示的向量,然后中间包括了6个卷积层和3个全连接层。新建model.py文件,具体代码如下:
import tensorflow as tf
import numpy as np
from sklearn.metrics import accuracy_score
class charCNN():
def __init__(self, num_classes):
self.char_nums = 72
self.num_classes = num_classes
self.dropout = 0.5
self.create_placeholder()
self.model()
def create_placeholder(self):
self.input = tf.placeholder(shape=[None, 1014], dtype=tf.int32, name="input")
self.labels = tf.placeholder(shape=[None], dtype=tf.int32, name="labels")
self.is_training = tf.placeholder(shape=[], dtype=tf.bool, name="is_training")
def conv_block(self, input, filters,kernel_size, is_max_pooling):
c = tf.layers.conv1d(inputs=input, filters=filters,
kernel_size=kernel_size,
padding="valid")
if is_max_pooling:
c = tf.layers.max_pooling1d(inputs=c,
pool_size=3,
strides=3,
padding="valid")
o = tf.nn.relu(c)
return o
def model(self):
input = tf.one_hot(self.input, self.char_nums)
o1 = self.conv_block(input, filters=256, kernel_size=7, is_max_pooling=True)
o2 = self.conv_block(o1, filters=256, kernel_size=7, is_max_pooling=True)
o3 = self.conv_block(o2, filters=256, kernel_size=3, is_max_pooling=False)
o4 = self.conv_block(o3, filters=256, kernel_size=3, is_max_pooling=False)
o5 = self.conv_block(o4, filters=256, kernel_size=3, is_max_pooling=False)
o6 = self.conv_block(o5, filters=256, kernel_size=3, is_max_pooling=True)
fc_input = tf.contrib.layers.flatten(o6)
fc_1 = tf.layers.dense(inputs=fc_input, units=1024, activation=tf.nn.relu)
fc_1 = tf.layers.dropout(inputs=fc_1, rate=self.dropout, training=self.is_training)
fc_2 = tf.layers.dense(inputs=fc_1, units=1024, activation=tf.nn.relu)
fc_2 = tf.layers.dropout(inputs=fc_2, rate=self.dropout, training=self.is_training)
output = tf.layers.dense(inputs=fc_2, units=self.num_classes)
labels = tf.one_hot(self.labels, depth=self.num_classes)
self.loss_op = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(labels=labels, logits=output)
)
self.train_op = tf.train.AdamOptimizer(learning_rate=0.0001).minimize(self.loss_op)
self.predict = tf.argmax(output, axis=1)
if __name__ == '__main__':
charCnn = charCNN(4)
3、训练和测试
新建main.py文件,具体代码如下
import tensorflow as tf
from get_data import data_load
from model import charCNN
import os
def train(self, sess, datas, labels, batch_size):
index = 0
while index < len(datas):
data_batch = datas[index:index+batch_size]
label_batch = labels[index:index+batch_size]
loss, _ = sess.run([self.loss_op, self.train_op],
feed_dict={
self.input: data_batch,
self.labels: label_batch,
self.is_training: True
})
if index % (batch_size*100) == 0:
print("Training Loss is:", loss)
index += batch_size
def test(self, sess, datas, labels, batch_size):
index = 0
results = []
while index < len(datas):
data_batch = datas[index:index+batch_size]
pred = sess.run(self.predict,
feed_dict={
self.input: data_batch,
self.is_training: False})
results += list(pred)
index += batch_size
acc = accuracy_score(labels, results)
return acc
data_load = data_load()
print(data_load.num_classes)
model = charCNN(num_classes=data_load.num_classes)
with tf.Session() as sess:
saver = tf.train.Saver()
sess.run(tf.global_variables_initializer())
best_dev_acc = 0
biggest_patient = 20
patient = 0
for i in range(100): # 迭代100轮
model.train(sess, data_load.train_datas, data_load.train_labels,32)
dev_acc = model.test(sess, data_load.dev_datas, data_load.dev_labels, 32)
if dev_acc > best_dev_acc:
best_dev_acc = dev_acc
saver.save(sess, "./model/best_result.ckpt")
patient=0
print("Epoch %d: best dev acc is updataed to %f"%(i, best_dev_acc))
else:
patient += 1
print("Epoch %d: best acc is not updated, the patient is %d"%(i,patient))
if patient == biggest_patient:
print("Patient is achieve biggest patient, training finished")
break
saver.restore(sess, "./model/best_result.ckpt")
test_acc = model.test(sess, data_load.test_datas, data_load.test_labels, 32)
print("Test acc in best dev acc is: ", test_acc)
在训练过程中用到了early-stoping策略,设置耐心值=20,每训练一次都要在dev集上测试准确率,如果在dev集上的准确率开始下降或者达到了最大耐心值,就停止训练。