好还是坏:人工智能二分类问题

Posted by RAIS on 2020-01-10

上一篇文章我们介绍了深度学习的 Hello World,代码写起来相比其他语言的 Hello World 有点多,且其背后的很多原理你可能还没有完全弄懂,但从宏观上来看,整体的思想是很好理解的。接下包括本篇在内的三篇文章,我们来用深度学习解决三个实际问题,也是非常经典的三个问题,分别是:

  1. 二分类问题(电影评论的好坏偏向性判断)
  2. 多分类问题(将新闻按照主题分类)
  3. 回归问题(根据房地产数据估算房地产价格)

我们解决这三个问题用到的训练模型为:

image

今天是第一篇,我们关注他们其中比较简单的二分类问题,代码在最后。

实际背景是 IMDB(互联网电影资料库,你可以理解为国外的豆瓣)有大量的关于电影的评论,但是由于数据量的问题,很难去人工手动判断一条评定是对电影的夸奖还是批评,这需要借助人工智能的帮助,根据用户评论的内容去判断用户的评论是积极的还是消极的(为使问题简化,我们取的数据是倾向性很明显的数据),数据集来自 IMDB,Numpy 数据格式:每一个单词对应一个索引数字,这样每一条评论就可以对应一个索引数字的序列,组成一维数组,也是一个一维向量,这样的多个一维向量组成二维数组,就对应对条评论。

背景介绍完了,整个过程分为如下几步,同时思考几个问题:

  1. 由于每一条评论的长度不一样,每个一维向量长度不一致,这样的数据不好处理,因此需要将数据进行预处理,这里采用 one-hot 方法(one-hot 是一个常用的方法,后面会有专门的文章介绍),简单点来 one-hot 做的事情就是:假如一个数组是[1, 3, 5, 3],需要处理成 10 维数据就是[0, 1, 0, 1, 0, 1, 0, 0, 0, 0],对应数数组索引处的数字是 1,其他数字是 0,本实际问题中,评论处理后的数据是一个向量(25000,10000)。

  2. 针对这种 one-hot 处理过的数据(0-1 类型的训练数据集),用带有 relu 激活的以 Dense 为中间层的网络进行深度学习表现很好(为什么这个表现的好,为什么 Dense 第一个参数即隐藏层的个数是 16 都是很复杂的问题,后续深入研究,这里只下结论将数据投影到 16 维空间中且是两个中间层的网络可以满足要求),随后一层是将输出格式化为 0-1 概率判断的结果。

  3. 针对损失函数,因为最后输出的结果是这条评论倾向性的概率值,所以选择交叉熵作为损失函数(binary_crossentropy,二元交叉熵对于这种情况的判断训练效果表现较好)。

  4. 最后,因为我们的模型是要做拟合,是一个有反馈的网络,从上图中可以看出来,因此这里选择部分数据用于提供反馈数据(反馈的意思是说先训练出一个模型,然后用另外一些数据进行测试,计算偏差,将偏差结果反馈给网络,网络进行参数调整,再一轮训练)且提供反馈的数据不应该用于训练,因此从训练集中取出一部分数据用于提供反馈。这里选择训练数据集中前一万条数据用于提供反馈数据,后一万五千条数据用于训练。

  5. 最后一步是启动 fit 训练模型,fit 中 validation_data 参数是用于提供验证数据的。最后我们从训练的过程中,提取出每一轮循环得到的数据画图直观看看训练损失、验证损失、训练精度和验证精度的具体变化:

image

image

我们从图中可以看出,随着训练网络迭代的次数越来越多,训练精度越来越大,训练损失越来越小,这是我们期望的;但是同时差不多在第四次迭代后,验证损失越来越大,验证精度越来越小,这就跟期望不相符了,这不科学,那这是为什么?

这是因为由于迭代次数过多,出现了过拟合。

随着训练数据迭代次数过多,训练出的模型为了更好的拟合训练集的数据,导致一些参数设置的会过于绝对,出现了过拟合,这是我们在训练网络中经常会遇到的问题,因此我们认为在迭代四次是比较好的,类似于数学概念上的极值,更多更少的迭代次数都不够好,因此我们将 fit 中的迭代次数改为 4,再次训练网络。

最后在另外的两万五千条测试集上进行验证,效果还可以,基本满足要求,问题得到解答。更多的细节已经写在了下面代码相关注释内容部分了,有兴趣请自行阅读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/env python3

import matplotlib.pyplot as plt
import numpy as np
from keras import layers
from keras import models
from keras.datasets import imdb


# IMDB 数据集,分类一个评论是正面的还是反面的
def comment():
# num_words = 10000 代表取前一万高频词,加入低频词数据量会过大且作用较小,暂不考虑
# 25000 条训练数据,25000 条测试数据
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

# 训练集被处理成整数数列,每条数据表示一条评论中单词出现的次数(如 good 出现次数是 5 次)
# [1, 14, 22, ... 19, 178, 32]
# print(train_data[0])

# 用 0 代表负面评论,1 代表正面评论
# print(train_labels[0])
# print(max([max(sequence) for sequence in train_data]))

# 单词与索引对照表
# word_index = imdb.get_word_index()
# reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
# decoded_review = ' '.join([reverse_word_index.get(i - 3, '?') for i in train_data[0]])

# 将评论翻译成可读语言
# this film was just brilliant casting location scenery story direction everyone's really suited the part they
# print(decoded_review)

# one-hot 方法预处理数据为向量
x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)
y_train = np.asarray(train_labels).astype('float32')
y_test = np.asarray(test_labels).astype('float32')

# 构造网络
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
# 创建编译模型
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
# model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='binary_crossentropy', metrics=['accuracy'])
# model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss=losses.binary_crossentropy,
# metrics=[metrics.binary_accuracy])

# 由于这里有反馈,因此需要有一定的数据进行数据进行验证,这里取前一万个数据进行验证操作
x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]

# validation_data 用于传递验证数据
history = model.fit(partial_x_train, partial_y_train, epochs=4, batch_size=512, validation_data=(x_val, y_val))

# History 是训练过程中的所有数据
history_dict = history.history
print(history_dict.keys())
history_dict = history.history
loss_values = history_dict['loss']
val_loss_values = history_dict['val_loss']
epochs = range(1, len(loss_values) + 1)

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.plot(epochs, loss_values, 'bo', label='训练损失')
plt.plot(epochs, val_loss_values, 'b', label='验证损失')
plt.title('训练和验证损失')
plt.xlabel('迭代')
plt.ylabel('损失')
plt.legend()
plt.show()

plt.clf()
acc = history_dict['acc']
val_acc = history_dict['val_acc']
plt.plot(epochs, acc, 'bo', label='训练精度')
plt.plot(epochs, val_acc, 'b', label='验证精度')
plt.title('训练和验证精度')
plt.xlabel('迭代')
plt.ylabel('精度')
plt.legend()
plt.show()

results = model.evaluate(x_test, y_test)
# [0.3329389461231232, 0.8639600276947021]
print(results)
# [[0.1754326], [0.9990357], [0.855113]...]
print(model.predict(x_test))


def vectorize_sequences(sequences, dimension=10000):
results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences):
results[i, sequence] = 1.
return results


if __name__ == "__main__":
comment()