0.数据集以及运行环境
数据集的地址:情绪分析的数据集,能稍微看懂英文就应该知道如何下载了
运行环境:Windows10,IDE:pycharm或者是Linux
0.数据预处理
"0","1467810369","Mon Apr 06 22:19:45 PDT 2009","NO_QUERY","_TheSpecialOne_","@switchfoot http://twitpic.com/2y1zl - Awww, that's a bummer. You shoulda got David Carr of Third Day to do it. ;D"
上图是原始数据的情况,总共有6个字段,主要是第一个字段(情感评价的结果)以及最后一个字段(tweet内容)是有用的。针对csv文件,我们使用pandas进行读取然后进行处理。处理的代码如下:
# 提取文件中的有用的字段
def userfull_filed(org_file, outuput_file):
data = pd.read_csv(os.path.join(data_dir, org_file), header=None, encoding='latin-1')
clf = data.values[:, 0]
content = data.values[:, -1]
new_clf = []
for temp in clf:
# 这个处理就是将情感评论结果进行所谓的one_hot编码
if temp == 0:
new_clf.append([1, 0]) # 消极评论
# elif temp == 2:
# new_clf.append([0, 1, 0]) # 中性评论
else:
new_clf.append([0, 1]) # 积极评论
df = pd.DataFrame(np.c_[new_clf, content], columns=['emotion0', 'emotion1', 'content'])
df.to_csv(os.path.join(data_dir, outuput_file), index=False)
这样处理的原因是,将情感评论值变成一个one-hot编码形式,这样我们进行nn或者是cnn处理的最后一层的输出单元为2(但有个巨大的bug那就是实际上训练集没有中性评论)
接下来利用nltk(这个简直是英语文本的自然语言的神奇呀)来生成单词集合,代码如下:
def sentence_english_manage(line):
# 英文句子的预处理
pattern = re.compile(r"[!#$%&'()*+,-./:;<=>?@[\]^_`{|}~0123456789]")
line = re.sub(pattern, '', line)
# line = [word for word in line.split() if word not in stopwords]
return line
def create_lexicon(train_file):
lemmatizer = WordNetLemmatizer()
df = pd.read_csv(os.path.join(data_dir, train_file))
count_word = {} # 统计单词的数量
all_word = []
for content in df.values[:, 2]:
words = word_tokenize(sentence_english_manage(content.lower())) # word_tokenize就是一个分词处理的过程
for word in words:
word = lemmatizer.lemmatize(word) # 提取该单词的原型
all_word.append(word) # 存储所有的单词
count_word = Counter(all_word)
# count_word = OrderetodDict(sorted(count_word.items(), key=lambda t: t[1]))
lex = []
for word in count_word.keys():
if count_word[word] < 100000 and count_word[word] > 100: # 过滤掉一些单词
lex.append(word)
with open('lexcion.pkl', 'wb') as file_write:
pickle.dump(lex, file_write)
return lex, count_word
最后生成一个只含有有用信息的文本内容
1.利用NN进行文本情感分类
1.1神经网络结构的搭建
在这里我设计了两层的隐藏层,单元数分别为1500/1500。神经网络的整个结构代码如下:
with open('lexcion.pkl', 'rb') as file_read:
lex = pickle.load(file_read)
n_input_layer = len(lex) # 输入层的长度
n_layer_1 = 1500 # 有两个隐藏层
n_layer_2 = 1500
n_output_layer = 2 # 输出层的大小
X = tf.placeholder(shape=(None, len(lex)), dtype=tf.float32, name="X")
Y = tf.placeholder(shape=(None, 2), dtype=tf.float32, name="Y")
batch_size = 500
dropout_keep_prob = tf.placeholder(tf.float32)
def neural_network(data):
layer_1_w_b = {
'w_': tf.Variable(tf.random_normal([n_input_layer, n_layer_1])),
'b_': tf.Variable(tf.random_normal([n_layer_1]))
}
layer_2_w_b = {
'w_': tf.Variable(tf.random_normal([n_layer_1, n_layer_2])),
'b_': tf.Variable(tf.random_normal([n_layer_2]))
}
layer_output_w_b = {
'w_': tf.Variable(tf.random_normal([n_layer_2, n_output_layer])),
'b_': tf.Variable(tf.random_normal([n_output_layer]))
}
# wx+b
# 这里有点需要注意那就是最后输出层不需要加激活函数
# 同时加入了dropout参数
full_conn_dropout_1 = tf.nn.dropout(data, dropout_keep_prob)
layer_1 = tf.add(tf.matmul(full_conn_dropout_1, layer_1_w_b['w_']), layer_1_w_b['b_'])
layer_1 = tf.nn.sigmoid(layer_1)
full_conn_dropout_2 = tf.nn.dropout(layer_1, dropout_keep_prob)
layer_2 = tf.add(tf.matmul(full_conn_dropout_2, layer_2_w_b['w_']), layer_2_w_b['b_'])
layer_2 = tf.nn.sigmoid(layer_2)
layer_output = tf.add(tf.matmul(layer_2, layer_output_w_b['w_']), layer_output_w_b['b_'])
# layer_output = tf.nn.softmax(layer_output)
return layer_output
比起我看的代码,这里我加入了dropout的参数,使得训练不会过拟合。但实际操作中,我发现加不加这个数据对于整个实验的影响并没有那麽大。
1.2获取训练与测试数据
原本的教程其实有一套自己的提取数据的过程,但我看了一眼感觉有些麻烦,我就自己写了一个数据提取的方法,代码如下:
def get_random_n_lines(i, data, batch_size):
# 从训练集中找训批量训练的数据
# 这里的逻辑需要理解,同时我们要理解要从积极与消极的两个集合中分层取样
if ((i * batch_size) % len(data) + batch_size) > len(data):
rand_index = np.arange(start=((i*batch_size) % len(data)),
stop=len(data))
else:
rand_index = np.arange(start=((i*batch_size) % len(data)),
stop=((i*batch_size) % len(data) + batch_size))
return data[rand_index, :]
def get_test_data(test_file):
# 获取测试集的数据用于测试
lemmatizer = WordNetLemmatizer()
df = pd.read_csv(os.path.join('data', test_file))
# groups = df.groupby('emotion1')
# group_neg_pos = groups.get_group(0).values # 获取非中性评论的信息
group_neg_pos = df.values
test_x = group_neg_pos[:, 2]
test_y = group_neg_pos[:, 0:2]
new_test_x = []
for tweet in test_x:
words = word_tokenize(tweet.lower())
words = [lemmatizer.lemmatize(word) for word in words]
features = np.zeros(len(lex))
for word in words:
if word in lex:
features[lex.index(word)] = 1
new_test_x.append(features)
return new_test_x, test_y
上面是提取训练集所需的代码,设计了一个提取一个batch_size大小的数据,因为我是分别从积极评论与消极评论中分别提取一定量的数据,所以要调用两次该代码。
下面这个则是提取测试集的代码,借用了词袋模型的思想,对一条tweet在字典长度的维度上进行一个编码过程。其实训练集也是这样处理的,但我写了两次有点傻。
1.3训练过程
借用别人博客中的代码,我进行了细微的修改,主要调整了代码的部分位置,添加了tensorboard的内容以及模型保存的内容,代码如下:
def train_neural_network():
# 配置tensorboard
tensorboard_dir = "tensorboard/nn"
if not os.path.exists(tensorboard_dir):
os.makedirs(tensorboard_dir)
# 损失函数
predict = neural_network(X)
cost_func = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=predict, labels=Y))
tf.summary.scalar("loss", cost_func)
optimizer = tf.train.AdamOptimizer().minimize(cost_func)
# 准确率
correct = tf.equal(tf.argmax(predict, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct, 'float'))
tf.summary.scalar("accuracy", accuracy)
merged_summary = tf.summary.merge_all()
writer = tf.summary.FileWriter(tensorboard_dir)
df = pd.read_csv(os.path.join('data', 'new_train_data.csv'))
# data = df.values
group_by_emotion0 = df.groupby('emotion0')
group_neg = group_by_emotion0.get_group(0).values
group_pos = group_by_emotion0.get_group(1).values
test_x, test_y = get_test_data('new_test_data.csv')
with tf.Session() as sess:
sess.run(tf.initialize_all_variables())
writer.add_graph(sess.graph)
lemmatizer = WordNetLemmatizer() # 判断词干所用的
saver = tf.train.Saver()
i = 0
# pre_acc = 0 # 存储前一次的准确率以和后一次的进行比较
while i < 5000:
rand_neg_data = get_random_n_lines(i, group_neg, batch_size)
rand_pos_data = get_random_n_lines(i, group_pos, batch_size)
rand_data = np.vstack((rand_neg_data, rand_pos_data)) # 矩阵合并
np.random.shuffle(rand_data) # 打乱顺序
batch_y = rand_data[:, 0:2] # 获取得分情况
batch_x = rand_data[:, 2] # 获取内容信息
new_batch_x = []
for tweet in batch_x:
words = word_tokenize(tweet.lower())
words = [lemmatizer.lemmatize(word) for word in words]
features = np.zeros(len(lex))
for word in words:
if word in lex:
features[lex.index(word)] = 1 # 一个句子中某个词可能出现两次,可以用+=1,其实区别不大
new_batch_x.append(features)
# batch_y = group_neg[:, 0: 3] + group_pos[:, 0: 3]
loss, _, train_acc = sess.run([cost_func, optimizer, accuracy],
feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.6})
if i % 100 == 0:
print("第{}次迭代,损失函数为{}, 训练的准确率为{}".format(i, loss, train_acc))
s = sess.run(merged_summary, feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.6})
writer.add_summary(s, i)
if i % 100 == 0:
# print(sess.run(accuracy, feed_dict={X: new_batch_x, Y: batch_y}))
test_acc = accuracy.eval({X: test_x[:200], Y: test_y[:200], dropout_keep_prob: 1.0})
print('测试集的准确率:', test_acc)
i += 1
if not os.path.isdir('./checkpoint'):
os.mkdir('./checkpoint')
saver.save(sess, './checkpoint/model.ckpt') # 保存session
其实这个过程没什么可说的,基本是一个套路。
1.4运行结果
我是在实验室的服务器上跑的上述代码,跑了几个小时,主要感觉还是模型的存储的时间比较长。因为整个模型的参数还是相当大的。最后的结果在tensorboard上显示结果为:
整体来说效果还可以。
2.利用CNN进行情感分类
这个项目实际上是一个CNN在NLP上的一个应用。一般而言,CNN是用来处理像图像、视频等矩阵类的东西,而他是如何应用到自然语言的呢?我们可以引入词嵌入矩阵这个概念,类似于Word2Vec这种东西,我们可以将一个词语或是字符拓展为一个长度为K的特征向量空间,这样借助词袋模型,我们就可以将一个句子或是文档拓展成一个矩阵。这样我们就可以引入卷积以及池化等方法来分析这些数据。
这里分享一个大神的博客,是专门针对CNN在NLP上的应用implementing-a-cnn-for-text-classification-in-tensorflow,通过这篇论文你可以充分了解如何利用tensorflow来实现分文分类以及cnn的一些解释。情绪分析与这个是基本一致的方法,我们也可以从这篇博客中详细了解整个流程。
接下来我们开始讲解CNN的情感分类的过程。从数据的预处理、CNN网络的构建、训练这三个方面进行讲解:
1.1神经网络的搭建:
这是一个重点的内容,我先引用一个图片,然后使用代码慢慢解释,图片如下:
图片有点大,顺便附上代码:
with open('lexcion.pkl', 'rb') as file_read:
lex = pickle.load(file_read)
input_size = len(lex) # 输入的长度
num_classes = 2 # 分类的数量
batch_size = 64
seq_length = 100 # 一个tweet的固定长度
X = tf.placeholder(tf.int32, [None, seq_length])
Y = tf.placeholder(tf.float32, [None, num_classes])
dropout_keep_prob = tf.placeholder(tf.float32)
def neural_network():
'''
整个流程的解释:
输入为一个X,shape=[None, 8057],8057为字典的长度
首先利用embedding_lookup的方法,将X转换为[None, 8057, 128]的向量,但有个疑惑就是emdeding_lookup的实际用法,在ceshi.py中有介绍
接着expande_dims使结果变成[None, 8057, 128, 1]的向量,但这样做的原因不是很清楚,原因就是通道数的设置
然后进行卷积与池化:
卷积核的大小有3种,每种卷积后的feature_map的数量为128
卷积核的shape=[3/4/5, 128, 1, 128],其中前两个为卷积核的长宽,最后一个为卷积核的数量,第三个就是通道数
卷积的结果为[None, 8057-3+1, 1, 128],矩阵的宽度已经变为1了,这里要注意下
池化层的大小需要注意:shape=[1, 8055, 1, 1]这样的化池化后的结果为[None, 1, 1, 128]
以上就是一个典型的文本CNN的过程
:return:
'''
''' 进行修改采用短编码 '''
''' tf.name_scope() 与 tf.variable_scope()的作用基本一致'''
with tf.name_scope("embedding"):
embedding_size = 64
'''
这里出现了一个问题没有注明上限与下限
'''
# embeding = tf.get_variable("embedding", [input_size, embedding_size]) # 词嵌入矩阵
embedding = tf.Variable(tf.random_uniform([input_size, embedding_size], -1.0, 1.0)) # 词嵌入矩阵
# with tf.Session() as sess:
# # sess.run(tf.initialize_all_variables())
# temp = sess.run(embedding)
embedded_chars = tf.nn.embedding_lookup(embedding, X)
embedded_chars_expanded = tf.expand_dims(embedded_chars, -1) # 设置通道数
# 卷积与池化层
num_filters = 256 # 卷积核的数量
filter_sizes = [3, 4, 5] # 卷积核的大小
pooled_outputs = []
for i, filter_size in enumerate(filter_sizes):
with tf.name_scope("conv_maxpool_{}".format(filter_size)):
filter_shape = [filter_size, embedding_size, 1, num_filters] # 要注意下卷积核大小的设置
W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1))
b = tf.Variable(tf.constant(0.1, shape=[num_filters]))
conv = tf.nn.conv2d(embedded_chars_expanded, W, strides=[1, 1, 1, 1], padding="VALID")
h = tf.nn.relu(tf.nn.bias_add(conv, b)) # 煞笔忘了加这个偏置的加法
pooled = tf.nn.max_pool(h, ksize=[1, seq_length - filter_size + 1, 1, 1],
strides=[1, 1, 1, 1], padding='VALID')
pooled_outputs.append(pooled)
num_filters_total = num_filters * len(filter_sizes)
'''
# tensor t3 with shape [2, 3]
# tensor t4 with shape [2, 3]
tf.shape(tf.concat([t3, t4], 0)) # [4, 3]
tf.shape(tf.concat([t3, t4], 1)) # [2, 6]
'''
h_pool = tf.concat(pooled_outputs, 3) # 原本是一个[None, 1, 1, 128]变成了[None, 1, 1, 384]
h_pool_flat = tf.reshape(h_pool, [-1, num_filters_total]) # 拉平处理 [None, 384]
# dropout
with tf.name_scope("dropout"):
h_drop = tf.nn.dropout(h_pool_flat, dropout_keep_prob)
# output
with tf.name_scope("output"):
# 这里就是最后的一个全连接层处理
# from tensorflow.contrib.layers import xavier_initializer
W = tf.get_variable("w", shape=[num_filters_total, num_classes],
initializer=tf.contrib.layers.xavier_initializer()) # 这个初始化要记住
b = tf.Variable(tf.constant(0.1, shape=[num_classes]))
output = tf.nn.xw_plus_b(h_drop, W, b)
# output = tf.nn.relu(output)
return output
在代码的最上面实际上比较详细的介绍了整个流程,首先这个卷积过程和一般的卷积过程不一样。卷积核是一个矩形的,其宽度与词嵌入矩阵的维度是一样的,他是在长度的方向进行卷积过程,实际含义就是寻找3个词(或者是5个或者是4个)词之间的特征属性,这样卷积的结果如上图所示成了一个1维的向量结果,然后我们进行一个max_pool操作,其他大小与卷积后生成的向量一样,最后我们得到了一个个1*1大小的向量。我们利用concat方法将这些向量合并,最后接入一个全连接层,然后softmax之后我们就可以得到分类的结果。整体流程在代码中有些,而且所有的tensor大小也注明了。
这里我需要说的是,一开始我使用的与NN一样的tweet向量化的方法,即将tweet映射到一个字典长度的维度上,但实验之后发现效果并不好,于是我再查看资料是发现了另一种文本向量化的方法,就是规定每条tweet长度固定均为100,这是在统计后得出的一个基本结果(我统计过训练集中tweet的长度一般在100以下,而且之后我还进行了数据的一个预处理,就是删除标点符号与数字),这样我们就可以将tweet映射到一个100维的向量上,这样矩阵就比较稠密点,而且训练的batch_size可以调的比较大,我们通常采用“多了截断/少了补充”的策略。
1.2就是最后的训练
在训练阶段,我顺便学习了tensorboard以及模型保存的方法,代码如下,基本上是一种常见的训练格式:
def train_neural_netword():
# 配置tensorboard
tensorboard_dir = "tensorboard/cnn"
if not os.path.exists(tensorboard_dir):
os.makedirs(tensorboard_dir)
output = neural_network()
# 构建准确率的计算过程
predictions = tf.argmax(output, 1)
correct_predictions = tf.equal(predictions, tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float")) # 强制转换
tf.summary.scalar("accuracy", accuracy)
# 构建损失函数的计算过程
optimizer = tf.train.AdamOptimizer(0.001)
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=Y))
tf.summary.scalar("loss", loss) # 将损失函数加入
grads_and_vars = optimizer.compute_gradients(loss)
train_op = optimizer.apply_gradients(grads_and_vars)
# 将参数保存如tensorboard中
merged_summary = tf.summary.merge_all()
writer = tf.summary.FileWriter(tensorboard_dir)
# 构建模型的保存模型
saver = tf.train.Saver(tf.global_variables())
# 数据集的获取
df = pd.read_csv(os.path.join('data', 'new_train_data.csv'))
group_by_emotion0 = df.groupby('emotion0')
group_neg = group_by_emotion0.get_group(0).values
group_pos = group_by_emotion0.get_group(1).values
test_x, test_y = get_test_data('new_test_data.csv')
with tf.Session() as sess:
sess.run(tf.initialize_all_variables())
# sess.run(tf.global_variables_initializer())
# 将图像加入tensorboard中
writer.add_graph(sess.graph)
lemmatizer = WordNetLemmatizer()
i = 0
pre_acc = 0
while i < 10000:
rand_neg_data = get_random_n_lines(i, group_neg, batch_size)
rand_pos_data = get_random_n_lines(i, group_pos, batch_size)
rand_data = np.vstack((rand_neg_data, rand_pos_data))
np.random.shuffle(rand_data)
batch_x = rand_data[:, 3]
batch_y = rand_data[:, 0: 3]
new_batch_x = []
for tweet in batch_x:
# 这段循环的意义就是将单词提取词干,并将字符转换成下标
words = word_tokenize(tweet.lower())
words = [lemmatizer.lemmatize(word) for word in words]
features = np.zeros(len(lex))
for word in words:
if word in lex:
features[lex.index(word)] = 1 # 一个句子中某个词可能出现两次,可以用+=1,其实区别不大
new_batch_x.append(features)
_, loss_ = sess.run([train_op, loss], feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.5})
if i % 20 == 0:
# 每二十次保存一次tensorboard
s = sess.run(merged_summary, feed_dict={X: new_batch_x, Y: batch_y, dropout_keep_prob: 0.5})
writer.add_summary(s, i)
if i % 10 == 0:
# 每10次打印出一个损失函数与准确率(这是指评测的准确率)
print(loss_)
accur = sess.run(accuracy, feed_dict={X: test_x, Y: test_y, dropout_keep_prob: 1.0})
if accur > pre_acc:
# 当前的准确率高于之前的准确率,更新模型
pre_acc = accur
print("准确率:", pre_acc)
tf.summary.scalar("accur", accur)
saver.save(sess, "cnn_model/model.ckpt")
i += 1
2.3运行结果
在tensorboard上可视化的结果如下:
说实话最后的训练效果并不好,我也不清楚是为什么,希望知道的同学告诉我一声吧。
3.后记
感谢CSDN用户“MachineLP”,我的整个流程是在他的基础上进行修改。实在是受益匪浅。
以上的代码会贴到我的github上(点击打开链接)