大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
个人主页-Sonhhxg_柒的博客_CSDN博客
欢迎各位→点赞 + 收藏⭐️ + 留言
系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟
在本章中,我们将构建一个模型,在给定一小段文本的情况下建议表情符号。我们将首先开发一个简单的情绪分类器,该分类器基于一组带有各种情绪(如幸福、爱情、惊喜等)标签的公共推文。我们将首先尝试使用贝叶斯分类器来了解基线性能并采取看看这个分类器能学到什么。然后,我们将切换到卷积网络,并研究调整此分类器的各种方法。
接下来,我们将看看如何使用 Twitter API 自己收集推文,然后我们将应用7.3节中的卷积模型,然后再转到词级模型。然后,我们将构建并应用循环词级网络,并比较三种不同的模型。
最后,我们会将所有三个模型组合成一个集成模型,该模型的性能优于这三个模型中的任何一个。
最终模型做得非常好,只需将其整合到移动应用程序中即可!
本章的代码可以在这些笔记本中找到:
07.1 文本分类
07.2 表情符号建议
07.3 推文嵌入
您如何确定一段文本中表达的情感?
找到一个由标记了情感的句子组成的数据集,并在它们上运行一个简单的分类器。
在尝试复杂的事情之前,最好先在现成的数据集上尝试我们能想到的最简单的事情。在这种情况下,我们将尝试基于已发布的数据集构建一个简单的情感分类器。在下面的食谱中,我们将尝试做一些更复杂的事情。
一个快速的谷歌搜索将我们带到一个来自 CrowdFlower 的包含推文和情绪标签的体面数据集。由于情绪标签在某种程度上类似于表情符号,这是一个好的开始。让我们下载文件并看一看:
import pandas as pd
from keras.utils.data_utils import get_file
import nb_utils
emotion_csv = get_file('text_emotion.csv',
'https://www.crowdflower.com/wp-content/'
'uploads/2016/07/text_emotion.csv')
emotion_df = pd.read_csv(emotion_csv)
emotion_df.head()
这导致:
推文ID | 情绪 | 作者 | 内容 |
---|---|---|---|
0 |
1956967341 |
空的 |
xoshayzers @tiffanylu 我知道我在听坏习惯… |
1 |
1956967666 |
悲伤 |
想要妈妈躺在床上,头疼 ughhhh……等着…… |
2 |
1956967696 |
悲伤 |
coolfunky 葬礼……阴沉的星期五…… |
3 |
1956967789 |
热情 |
czareaquino 想尽快和朋友一起出去玩! |
4 |
1956968416 |
中性的 |
xkilljoyx @dannycastillo 我们想和某人交易... |
我们还可以检查各种情绪发生的频率:
emotion_df['sentiment'].value_counts()
neutral 8638 worry 8459 happiness 5209 sadness 5165 love 3842 surprise 2187
一些最简单的模型通常会产生令人惊讶的好结果,这些模型来自朴素贝叶斯家族。我们将从使用sklearn
提供的方法对数据进行编码开始。TfidfVectorizer
根据单词的逆文档频率为单词分配权重;出现的单词通常会获得较低的权重,因为它们往往信息量较少。LabelEncoder
将唯一整数分配给它看到的不同标签:
tfidf_vec = TfidfVectorizer(max_features=VOCAB_SIZE)
label_encoder = LabelEncoder()
linear_x = tfidf_vec.fit_transform(emotion_df['content'])
linear_y = label_encoder.fit_transform(emotion_df['sentiment'])
有了这些数据,我们现在可以构建贝叶斯模型并对其进行评估:
bayes = MultinomialNB()
bayes.fit(linear_x, linear_y)
pred = bayes.predict(linear_x)
precision_score(pred, linear_y, average='micro')
0.28022727272727271
我们得到了 28% 的正确率。如果我们总是预测最有可能的类别,我们会得到略高于 20% 的结果,所以我们有了一个良好的开端。还有一些其他简单的分类器可以尝试,它们可能会做得更好,但往往更慢:
bayes = MultinomialNB()
bayes.fit(linear_x, linear_y)
pred = bayes.predict(linear_x)
precision_score(pred, linear_y, average='micro')
random_forrest 0.283939393939 svm 0.218636363636 sgd 0.325454545455
尝试“可能可行的最简单的事情”可以帮助我们快速入门,并让我们了解数据中是否有足够的信号来完成我们想做的工作。
贝叶斯分类器在打击垃圾邮件的早期阶段被证明非常有效。然而,他们假设每个因素的贡献是相互独立的——所以在这种情况下,推文中的每个词对预测标签都有一定的影响,独立于其他词——显然情况并非总是如此。一个简单的例子是在句子中插入单词not可以否定表达的情绪。该模型仍然很容易构建,并且很快就能得到结果,而且结果是可以理解的。通常,如果贝叶斯模型不能对您的数据产生任何好的结果,那么使用更复杂的模型可能不会有太大帮助。
笔记
贝叶斯模型似乎比我们天真的预期的效果更好。关于为什么会这样,有一些有趣的研究。在机器学习之前,他们帮助破解了 Enigma 代码,并帮助推动了第一个垃圾邮件检测器。
你怎么能看到一个简单的分类器学到了什么?
查看使分类器输出结果的促成因素。
使用贝叶斯方法的优点之一是我们得到了一个我们可以理解的模型。正如我们在上一个秘籍中所讨论的,贝叶斯模型假设每个词的贡献独立于其他词,因此要了解我们的模型学到了什么,我们可以询问模型对单个词的看法。
现在请记住,该模型需要一系列文档,每个文档都编码为一个向量,其长度等于词汇表的大小,每个元素都编码该文档中相应单词相对于所有文档的相对频率。因此,每个仅包含一个单词的文档集合将是一个对角线上的方阵;第n个文档对于词汇表中的所有单词都将为零,除了单词n。现在我们可以为每个单词预测标签的可能性:
d = eye(len(tfidf_vec.vocabulary_))
word_pred = bayes.predict_proba(d)
然后我们可以遍历所有预测并找到每个类的单词分数。我们将其存储在一个Counter
对象中,以便我们可以轻松访问贡献最多的单词:
by_cls = defaultdict(Counter)
for word_idx, pred in enumerate(word_pred):
for class_idx, score in enumerate(pred):
cls = label_encoder.classes_[class_idx]
by_cls[cls][inverse_vocab[word_idx]] = score
让我们打印结果:
for k in by_cls:
words = [x[0] for x in by_cls[k].most_common(5)]
print(k, ':', ' '.join(words))
happiness : excited woohoo excellent yay wars hate : hate hates suck fucking zomberellamcfox boredom : squeaking ouuut cleanin sooooooo candyland3 enthusiasm : lena_distractia foolproofdiva attending krisswouldhowse tatt fun : xbox bamboozle sanctuary oldies toodaayy love : love mothers mommies moms loved surprise : surprise wow surprised wtf surprisingly empty : makinitrite conversating less_than_3 shakeyourjunk kimbermuffin anger : confuzzled fridaaaayyyyy aaaaaaaaaaa transtelecom filthy worry : worried poor throat hurts sick relief : finally relax mastered relief inspiration sadness : sad sadly cry cried miss neutral : www painting souljaboytellem link frenchieb
在深入研究更复杂的东西之前检查一个简单的模型学到了什么是一个有用的练习。尽管深度学习模型很强大,但事实是很难真正说出它们在做什么。我们可以大致了解它们的工作原理,但要真正理解训练产生的数百万个权重几乎是不可能的。
我们的贝叶斯模型的结果与我们的预期一致。“悲伤”一词表示“悲伤”,“哇”表示惊讶。感人的是,“母亲”这个词是爱的强烈暗示。
我们确实看到了一堆奇怪的词,比如“kimbermuffin”和“makinitrite”。检查后发现这些是 Twitter 句柄。“万无一失”只是一个非常热心的人。根据目标,我们可能会考虑将这些过滤掉。
您想尝试使用深度网络来确定使用深度网络在一段文本中表达的情绪。
使用卷积网络。
CNN 更常与图像识别相关联(参见第 9 章),但它们也能很好地处理某些文本分类任务。这个想法是在文本上滑动一个窗口,然后将一系列项目转换为(更短的)特征序列。在这种情况下,项目将是字符。每一步都使用相同的权重,因此我们不必多次学习相同的东西——“猫”一词在推文中的任何位置都表示“猫”:
char_input = Input(shape=(max_sequence_len, num_chars), name='input')
conv_1x = Conv1D(128, 6, activation='relu', padding='valid')(char_input)
max_pool_1x = MaxPooling1D(6)(conv_1x)
conv_2x = Conv1D(256, 6, activation='relu', padding='valid')(max_pool_1x)
max_pool_2x = MaxPooling1D(6)(conv_2x)
flatten = Flatten()(max_pool_2x)
dense = Dense(128, activation='relu')(flatten)
preds = Dense(num_labels, activation='softmax')(dense)
model = Model(char_input, preds)
model.compile(loss='sparse_categorical_crossentropy',
optimizer='rmsprop',
metrics=['acc'])
为了使模型运行,我们首先必须对数据进行矢量化。我们将使用我们在上一个秘籍中看到的相同的 one-hot 编码,将每个字符编码为一个全零填充的向量,除了第n个条目,其中n对应于我们正在编码的字符:
chars = list(sorted(set(chain(*emotion_df['content']))))
char_to_idx = {ch: idx for idx, ch in enumerate(chars)}
max_sequence_len = max(len(x) for x in emotion_df['content'])
char_vectors = []
for txt in emotion_df['content']:
vec = np.zeros((max_sequence_len, len(char_to_idx)))
vec[np.arange(len(txt)), [char_to_idx[ch] for ch in txt]] = 1
char_vectors.append(vec)
char_vectors = np.asarray(char_vectors)
char_vectors = pad_sequences(char_vectors)
labels = label_encoder.transform(emotion_df['sentiment'])
让我们将数据分成训练集和测试集:
def split(lst):
training_count = int(0.9 * len(char_vectors))
return lst[:training_count], lst[training_count:]
training_char_vectors, test_char_vectors = split(char_vectors)
training_labels, test_labels = split(labels)
我们现在可以训练模型并对其进行评估:
char_cnn_model.fit(training_char_vectors, training_labels,
epochs=20, batch_size=1024)
char_cnn_model.evaluate(test_char_vectors, test_labels)
20 epochs 后,训练准确率达到 0.39,但测试准确率只有 0.31。差异通过过度拟合来解释;该模型不仅学习了也适用于测试集的数据的一般方面,而且开始记忆部分训练数据。这类似于学生在不了解原因的情况下学习答案匹配哪些问题。
卷积网络在我们希望我们的网络独立于它们发生的地方学习事物的情况下工作得很好。对于图像识别,我们不希望网络为每个像素单独学习;我们希望它学会独立于图像中出现的位置来识别特征。
同样,对于文本,我们希望模型知道如果“爱”这个词出现在推文中的任何地方,“爱”将是一个很好的标签。我们不希望模型分别为每个位置学习这一点。CNN 通过在文本上运行一个滑动窗口来实现这一点。在这种情况下,我们使用大小为 6 的窗口,因此我们一次取 6 个字符;对于包含 125 个字符的推文,我们会应用 120 次。
关键是这 120 个神经元中的每一个都使用相同的权重,所以它们都学习相同的东西。在卷积之后,我们应用一个max_pooling
层。该层将采用六个神经元组并输出其激活的最大值。我们可以认为这是将任何神经元具有的最强理论转发到下一层。它还将尺寸减小了六倍。
在我们的模型中,我们有两个卷积/最大池化层,它们将大小从 167×100 的输入更改为 3×256。我们可以将这些视为增加抽象级别的步骤。在输入级别,我们只知道 167 个位置中的每一个位置出现了 100 个不同字符中的哪一个。在最后一次卷积之后,我们有 3 个向量,每个向量 256,它们对推文开头、中间和结尾发生的事情进行编码。
如何自动收集大量 Twitter 数据用于培训目的?
使用推特 API。
首先要做的是前往https://apps.twitter.com注册一个新应用程序。单击创建新应用程序按钮并填写表格。我们不会代表用户做任何事情,因此您可以将回调 URL 字段留空。
完成后,您应该拥有允许访问 API 的两个密钥和两个秘密。让我们将它们存储在相应的变量中:
CONSUMER_KEY = ''
CONSUMER_SECRET = ''
ACCESS_TOKEN = ''
ACCESS_SECRET = ''
我们现在可以构造一个身份验证对象:
auth=twitter.OAuth(
consumer_key=CONSUMER_KEY,
consumer_secret=CONSUMER_SECRET,
token=ACCESS_TOKEN,
token_secret=ACCESS_SECRET,
)
Twitter API 有两个部分。REST API 可以调用各种函数来搜索推文、获取用户状态,甚至发布到 Twitter。不过,在这个秘籍中,我们将使用流 API。
如果您向 Twitter 付费,您将获得一个包含所有正在发生的推文的流。如果您不付款,您将获得所有推文的样本。这对我们的目的来说已经足够了:
status_stream = twitter.TwitterStream(auth=auth).statuses
该stream
对象有一个迭代器 ,sample
它将产生推文。让我们来看看其中的一些使用itertools.islice
:
[x['text'] for x in itertools.islice(stream.sample(), 0, 5) if x.get('text')]
在这种情况下,我们只需要英文且包含至少一个表情符号的推文:
def english_has_emoji(tweet):
if tweet.get('lang') != 'en':
return False
return any(ch for ch in tweet.get('text', '') if ch in emoji.UNICODE_EMOJI)
我们现在可以获得一百条包含至少一个表情符号的推文:
tweets = list(itertools.islice(
filter(english_has_emoji, status_stream.sample()), 0, 100))
我们每秒会收到 2 到 3 条推文,这还不错,但是我们需要一段时间才能获得相当大的训练集。我们只关心只有一种表情符号的推文,我们只想保留那个表情符号和文本:
stripped = []
for tweet in tweets:
text = tweet['text']
emojis = {ch for ch in text if ch in emoji.UNICODE_EMOJI}
if len(emojis) == 1:
emoiji = emojis.pop()
text = ''.join(ch for ch in text if ch != emoiji)
stripped.append((text, emoiji))
Twitter 可以成为非常有用的训练数据来源。每条推文都有大量与之相关的元数据,从发布推文的帐户到图像和哈希标签。在本章中,我们只使用语言元信息,但它是一个值得探索的丰富领域。
你如何预测最匹配一段文字的表情符号?
重新利用7.3节中的情绪分类器。
如果您在上一步中收集了大量推文,则可以使用这些推文。如果没有,您可以在data/emojis.txt中找到一个很好的示例。让我们将它们读入 Pandas DataFrame
。我们将过滤掉出现次数少于 1000 次的任何表情符号:
all_tweets = pd.read_csv('data/emojis.txt',
sep='\t', header=None, names=['text', 'emoji'])
tweets = all_tweets.groupby('emoji').filter(lambda c:len(c) > 1000)
tweets['emoji'].value_counts()
该数据集太大而无法以矢量化形式保存在内存中,因此我们将使用生成器进行训练。Pandas 提供了一种方便的sample
方法,它允许我们拥有以下内容data_generator
:
def data_generator(tweets, batch_size):
while True:
batch = tweets.sample(batch_size)
X = np.zeros((batch_size, max_sequence_len, len(chars)))
y = np.zeros((batch_size,))
for row_idx, (_, row) in enumerate(batch.iterrows()):
y[row_idx] = emoji_to_idx[row['emoji']]
for ch_idx, ch in enumerate(row['text']):
X[row_idx, ch_idx, char_to_idx[ch]] = 1
yield X, y
现在,我们可以使用7.3来训练模型而无需修改:
train_tweets, test_tweets = train_test_split(tweets, test_size=0.1)
BATCH_SIZE = 512
char_cnn_model.fit_generator(
data_generator(train_tweets, batch_size=BATCH_SIZE),
epochs=20,
steps_per_epoch=len(train_tweets) / BATCH_SIZE,
verbose=2
)
该模型训练到大约 40% 的精度。这听起来不错,即使我们考虑到顶部表情符号比底部表情符号出现的频率更高。如果我们在评估集上运行模型,精度分数会从 40% 下降到略高于 35%:
char_cnn_model.evaluate_generator(
data_generator(test_tweets, batch_size=BATCH_SIZE),
steps=len(test_tweets) / BATCH_SIZE
)
[3.0898117224375405, 0.35545459692028986]
在模型本身没有变化的情况下,我们可以为推文推荐表情符号,而不是运行情绪分类。这并不奇怪。在某种程度上,表情符号是作者应用的情感标签。两个任务的性能大致相同,这可能不太值得期待,因为我们有更多的标签,而且我们预计标签会更嘈杂。
如何提高网络性能?
在引入 dropout 的同时增加可训练变量的数量,这种技术使更大的网络更难过拟合。
增加神经网络表达能力的简单方法是使其更大,或者通过使单个层更大或向网络添加更多层。具有更多变量的网络具有更高的学习能力并且可以更好地概括。不过,这不是免费的。在某些时候,网络开始过度拟合。(1.3 节更详细地描述了这个问题。)
让我们从扩展我们当前的网络开始。在前面的秘籍中,我们为卷积使用了 6 的步长。六个字符似乎是捕获本地信息的合理数量,但也有点随意。为什么不是四五个?事实上,我们可以做到这三个,然后加入结果:
layers = []
for window in (4, 5, 6):
conv_1x = Conv1D(128, window, activation='relu',
padding='valid')(char_input)
max_pool_1x = MaxPooling1D(4)(conv_1x)
conv_2x = Conv1D(256, window, activation='relu',
padding='valid')(max_pool_1x)
max_pool_2x = MaxPooling1D(4)(conv_2x)
layers.append(max_pool_2x)
merged = Concatenate(axis=1)(layers)
在使用这个带有额外层的网络进行训练期间,精度提高了 47%。但不幸的是,测试集上的精度仅达到 37%。这仍然比我们之前的稍微好一点,但是过度拟合的差距增加了很多。
有许多技术可以阻止过度拟合,它们都有一个共同点,即它们限制了模型可以学习的内容。最受欢迎的方法之一是添加Dropout
图层。在训练期间,Dropout
将所有神经元的一部分的权重随机设置为零。这迫使网络更加稳健地学习,因为它不能依赖特定的神经元存在。在预测过程中,所有神经元都起作用,这会平均结果并降低异常值的可能性。这会减慢过度拟合的速度。
在 Keras 中,我们Dropout
像添加任何其他层一样添加。我们的模型就变成了:
for window in (4, 5, 6):
conv_1x = Conv1D(128, window,
activation='relu', padding='valid')(char_input)
max_pool_1x = MaxPooling1D(4)(conv_1x)
dropout_1x = Dropout(drop_out)(max_pool_1x)
conv_2x = Conv1D(256, window,
activation='relu', padding='valid')(dropout_1x)
max_pool_2x = MaxPooling1D(4)(conv_2x)
dropout_2x = Dropout(drop_out)(max_pool_2x)
layers.append(dropout_2x)
merged = Concatenate(axis=1)(layers)
dropout = Dropout(drop_out)(merged)
选择 dropout 值是一门艺术。较高的值意味着模型更健壮,但训练速度也更慢。以 0.2 运行使训练精度达到 0.43,测试精度达到 0.39,这表明我们仍然可以走得更高。
这个秘籍提供了一些我们可以用来提高网络性能的技术的想法。通过添加更多层,尝试不同的窗口,并Dropout
在不同的地方引入层,我们有很多旋钮可以优化我们的网络。找到最佳值的过程称为超参数调整。
有些框架可以通过尝试各种组合来自动找到最佳参数。由于他们确实需要多次训练模型,因此您需要耐心或访问多个实例来并行训练您的模型。
推文是文字,而不仅仅是随机字符。你怎么能利用这个事实呢?
训练一个将词嵌入序列而不是字符序列作为输入的模型。
首先要做的是标记我们的推文。我们将构建一个标记器来保留前 50,000 个单词,将其应用于我们的训练和测试集,然后填充两者,使它们具有统一的长度:
VOCAB_SIZE = 50000
tokenizer = Tokenizer(num_words=VOCAB_SIZE)
tokenizer.fit_on_texts(tweets['text'])
training_tokens = tokenizer.texts_to_sequences(train_tweets['text'])
test_tokens = tokenizer.texts_to_sequences(test_tweets['text'])
max_num_tokens = max(len(x) for x in chain(training_tokens, test_tokens))
training_tokens = pad_sequences(training_tokens, maxlen=max_num_tokens)
test_tokens = pad_sequences(test_tokens, maxlen=max_num_tokens)
我们可以通过使用预训练嵌入来快速启动我们的模型(参见第 3 章)。我们将使用实用函数 加载权重,该函数load_wv2
将加载 Word2vec 嵌入并将它们与我们语料库中的单词进行匹配。这将为每个包含 Word2vec 模型权重的标记构造一个矩阵:
def load_w2v(tokenizer=None):
w2v_model = gensim.models.KeyedVectors.load_word2vec_format(
word2vec_vectors, binary=True)
total_count = sum(tokenizer.word_counts.values())
idf_dict = {k: np.log(total_count/v)
for (k,v) in tokenizer.word_counts.items()}
w2v = np.zeros((tokenizer.num_words, w2v_model.syn0.shape[1]))
idf = np.zeros((tokenizer.num_words, 1))
for k, v in tokenizer.word_index.items():
if < tokenizer.num_words and k in w2v_model:
w2v[v] = w2v_model[k]
idf[v] = idf_dict[k]
return w2v, idf
我们现在可以创建一个与我们的角色模型非常相似的模型,主要只是改变我们处理输入的方式。我们的输入采用一系列标记,嵌入层在我们刚刚创建的矩阵中查找每个标记:
message = Input(shape=(max_num_tokens,), dtype='int32', name='title')
embedding = Embedding(mask_zero=False, input_dim=vocab_size,
output_dim=embedding_weights.shape[1],
weights=[embedding_weights],
trainable=False,
name='cnn_embedding')(message)
该模型有效,但不如角色模型。我们可以摆弄各种超参数,但差距相当大(字符级模型的精度为 38%,而单词级模型的精度为 30%)。我们可以更改的一件事确实会产生影响——将嵌入层的trainable
属性设置为True
. 这有助于将词级模型的精度提高到 36%,但这也意味着我们使用了错误的嵌入。我们将在下一个秘籍中看看如何解决这个问题。
词级模型比字符级模型具有更大的输入数据视图,因为它查看的是词簇而不是字符簇。我们没有使用我们用于字符的 one-hot 编码,而是使用词嵌入来快速开始。在这里,我们用一个向量来表示每个词,该向量表示该词的语义值作为模型的输入。(有关词嵌入的更多信息,请参阅第 3 章。)
这个秘籍中展示的模型并没有优于我们的字符级模型,也没有比我们在秘籍 7.1中看到的贝叶斯模型好多少。这表明我们预训练的词嵌入的权重与我们的问题不匹配。如果我们将嵌入层设置为可训练,事情会变得更好;如果我们允许它改变这些嵌入,模型就会得到改进。我们将在下一个秘籍中更详细地了解这一点。
权重不是很好的匹配并不令人惊讶。Word2vec 模型是在 Google News 上训练的,它使用的语言与我们在社交媒体上平均发现的完全不同。例如,流行的主题标签不会出现在 Google 新闻语料库中,但它们似乎对于分类推文相当重要。
如何获得与您的语料库匹配的词嵌入?
训练你自己的词嵌入。
该gensim
软件包不仅允许我们使用预训练的嵌入模型,还可以训练新的嵌入。它唯一需要做的就是生成令牌序列的生成器。它将使用它来建立一个词汇表,然后通过多次通过生成器来继续训练模型。以下对象将通过推文流,清理它们并标记它们:
class TokensYielder(object):
def __init__(self, tweet_count, stream):
self.tweet_count = tweet_count
self.stream = stream
def __iter__(self):
print('!')
count = self.tweet_count
for tweet in self.stream:
if tweet.get('lang') != 'en':
continue
text = tweet['text']
text = html.unescape(text)
text = RE_WHITESPACE.sub(' ', text)
text = RE_URL.sub(' ', text)
text = strip_accents(text)
text = ''.join(ch for ch in text if ord(ch) < 128)
if text.startswith('RT '):
text = text[3:]
text = text.strip()
if text:
yield text_to_word_sequence(text)
count -= 1
if count <= 0:
break
我们现在可以训练模型了。明智的做法是收集一周左右的推文,将它们保存在一组文件中(每行一个 JSON 文档是一种流行的格式),然后将一个生成器传递给TokensYielder
.
在我们开始这样做并等待一周让我们的推文进入之前,我们可以通过仅获取 100,000 条过滤推文来测试这是否有效:
tweets = list(TokensYielder(100000,
twitter.TwitterStream(auth=auth).statuses.sample()))
然后构建模型:
model = gensim.models.Word2Vec(tweets, min_count=2)
查看“love”这个词的最近邻,我们发现我们确实有自己的特定领域嵌入——只有在 Twitter 上“453”与“love”相关,因为在线它是“cool story,bro”的缩写:
model.wv.most_similar(positive=['love'], topn=5)
[('hate', 0.7243724465370178), ('loved', 0.7227891087532043), ('453', 0.707709789276123), ('melanin', 0.7069753408432007), ('appreciate', 0.696381688117981)]
“黑色素”的预期略低。
使用现有的词嵌入是快速入门的好方法,但仅适用于我们正在处理的文本与训练嵌入的文本相似的情况。在情况并非如此并且我们可以访问与我们正在训练的内容相似的大量文本的情况下,我们可以轻松地训练自己的词嵌入。
正如我们在上一个秘籍中看到的,训练新嵌入的另一种方法是采用现有的嵌入,但将trainable
层的属性设置为True
。这将使网络调整嵌入层中单词的权重并找到它们丢失的新单词。
当然,有一种方法可以利用推文是一系列单词这一事实。你怎么能做到这一点?
使用词级循环网络进行分类。
卷积网络非常适合在输入流中发现局部模式。对于情绪分析,这通常效果很好。某些短语会独立于它们出现的位置影响句子的情绪。然而,建议表情符号的任务中有一个时间元素,我们没有利用使用 CNN 的优势。与推文相关的表情符号通常是推文的结论。在这种情况下,RNN 可能更适合。
我们在第 5 章中看到了如何教 RNN 生成文本。我们可以使用类似的方法来推荐表情符号。就像单词级 CNN 一样,我们将输入转换为嵌入的单词。一层 LSTM 做得很好:
def create_lstm_model(vocab_size, embedding_size=None, embedding_weights=None):
message = layers.Input(shape=(None,), dtype='int32', name='title')
embedding = Embedding(mask_zero=False, input_dim=vocab_size,
output_dim=embedding_weights.shape[1],
weights=[embedding_weights],
trainable=True,
name='lstm_embedding')(message)
lstm_1 = layers.LSTM(units=128, return_sequences=False)(embedding)
category = layers.Dense(units=len(emojis), activation='softmax')(lstm_1)
model = Model(
inputs=[message],
outputs=[category],
)
model.compile(loss='sparse_categorical_crossentropy',
optimizer='rmsprop', metrics=['accuracy'])
return model
在 10 个 epoch 之后,我们在训练集上达到了 50% 的精度,在测试集上达到了 40% 的精度,大大优于 CNN 模型。
我们在这里使用的 LSTM 模型大大优于我们的单词级 CNN。我们可以将这种卓越的性能归因于推文是序列的事实,在推文结尾发生的事情与开头发生的事情有不同的影响。
由于我们的字符级 CNN 往往比我们的单词级 CNN 做得更好,我们的单词级 LSTM 比我们的字符级 CNN 做得更好,我们可能想知道字符级 LSTM 是否会更好。事实证明不是。
这样做的原因是,如果我们一次为 LSTM 提供一个字符,它会在推文结束时忘记在推文开头发生的事情。如果我们一次给 LSTM 一个单词,它就能克服这个问题。另请注意,我们的字符级 CNN 实际上并不一次处理输入一个字符。我们一次使用四个、五个或六个字符的序列,并将多个卷积堆叠在一起,这样平均推文在最高级别只剩下三个特征向量。
不过,我们可以尝试将两者结合起来,方法是创建一个 CNN,将推文压缩成具有更高抽象级别的片段,然后将这些向量输入 LSTM 以得出最终结论。这当然接近于我们的单词级 LSTM 的工作方式。我们没有使用 CNN 对文本片段进行分类,而是使用预训练的词嵌入在每个词的级别上做同样的事情。
您想直观地了解您构建的不同模型在实践中的比较情况。
使用 Pandas 显示他们同意和不同意的地方。
Precision 让我们了解模型的表现如何。不过,建议表情符号是一项相当嘈杂的任务,因此查看我们的各种模型如何并排执行可能非常有用。Pandas 是一个很好的工具。
让我们首先将角色模型的测试数据作为向量而不是生成器获取:
test_char_vectors, _ = next(data_generator(test_tweets, None))
现在让我们对前 100 个项目进行预测:
predictions = {
label: [emojis[np.argmax(x)] for x in pred]
for label, pred in (
('lstm', lstm_model.predict(test_tokens[:100])),
('char_cnn', char_cnn_model.predict(test_char_vectors[:100])),
('cnn', cnn_model.predict(test_tokens[:100])),
)
}
现在我们可以DataFrame
在推文文本和原始表情符号旁边构建并显示每个模型的前 25 个预测的 Pandas:
pd.options.display.max_colwidth = 128
test_df = test_tweets[:100].reset_index()
eval_df = pd.DataFrame({
'content': test_df['text'],
'true': test_df['emoji'],
**predictions
})
eval_df[['content', 'true', 'char_cnn', 'cnn', 'lstm']].head(25)
这导致:
# | content | true | char_cnn | lstm | |
---|---|---|---|---|---|
0 | @Gurmeetramrahim @RedFMIndia @rjraunac #8DaysToLionHeart | ||||
1 | @suchsmallgods 我迫不及待想向他展示这些推文 | ||||
2 | @Captain_RedWolf 我前面有 20 组大声笑 WAYYYYYY | ||||
3 | @OtherkinOK 刚刚在@EPfestival,真是一套!下一站是 2016 年 11 月 11 日星期五的@whelanslive。 | ||||
4 | @jochendria:KathNiel 和 GForce Jorge。#PushAwardsKathNiels | ||||
5 | 好的 | ||||
6 | “心烦意乱的意思是不高兴” “所以这意味着困惑对吧?” -@ReevesDakota | ||||
7 | @JennLiri babe wtf call bck 我试着听这个铃声 | ||||
8 | Jen想交朋友吗?我们可以成为朋友。爱你,女孩。#BachelorInParadise | ||||
9 | @amwalker38:去关注这些热门帐户@the1stMe420 @DanaDeelish @So_deelish @aka_teemoney38 @CamPromoXXX @SexyLThings @l ... | ||||
10 | @gspisak:我总是取笑那些早到 30 分钟以上来接孩子的父母,这就是我,至少我得到了一个…… | ||||
11 | @ShawnMendes:多伦多广告牌。非常酷!@spotify #ShawnXSpotify 去你的城市找他们 | ||||
12 | @kayleeburt77 我可以要你的电话号码吗?我好像失去了我的。 | ||||
13 | @KentMurphy:蒂姆·蒂博在职业球赛中看到的第一个球场就击中了一个小球 | ||||
14 | @HailKingSoup... | ||||
15 | @RoxeteraRibbons 相同,我必须想办法证明这一点 | ||||
16 | @theseoulstory:9 月回归:2PM、SHINee、INFINITE、BTS、Red Velvet、Gain、宋智恩、关东…… | ||||
17 | @VixenMusicLabel - 和平与爱 | ||||
18 | @iDrinkGallons 抱歉 | ||||
19 | @StarYouFollow:19-弗里森 | ||||
20 | @RapsDaiIy:不要睡在丑陋的上帝身上 | ||||
21 | 我所有的班次怎么这么快就接班了?!重量级 | ||||
22 | @ShadowhuntersTV:#Shadowhunters 的粉丝们,你会给这个父女多少 s #FlashbackFriday 之间的亲密时刻...... | ||||
23 | @mbaylisxo:感谢上帝,我有制服,不用担心每天穿什么 | ||||
24 | 情绪波动就像... |
浏览这些结果,我们可以看到,当模型经常出错时,它们会落在一个与原始推文中的表情非常相似的表情符号上。有时预测似乎比实际使用的更有意义,有时没有一个模型做得很好。
查看实际数据可以帮助我们了解模型哪里出错了。在这种情况下,提高性能的一个简单方法是将所有相似的表情符号视为相同。不同的心形和不同的笑脸表达了或多或少相同的东西。
一种选择是学习表情符号的嵌入。这将使我们了解表情符号的相关性。然后我们可以有一个将这种相似性考虑在内的损失函数,而不是硬性的正确/错误度量。
您想利用模型的综合预测能力来获得更好的答案。
将模型组合成一个集成模型。
群体智慧的理念——群体意见的平均值通常比任何特定意见更准确——也适用于机器学习模型。Average
我们可以通过使用三个输入并使用Keras的层组合模型的输出,将所有三个模型合二为一:
def prediction_layer(model):
layers = [layer for layer in model.layers
if layer.name.endswith('_predictions')]
return layers[0].output
def create_ensemble(*models):
inputs = [model.input for model in models]
predictions = [prediction_layer(model) for model in models]
merged = Average()(predictions)
model = Model(
inputs=inputs,
outputs=[merged],
)
model.compile(loss='sparse_categorical_crossentropy',
optimizer='rmsprop',
metrics=['accuracy'])
return model
我们需要一个不同的数据生成器来训练这个模型;我们现在有三个,而不是指定一个输入。由于它们有不同的名称,我们可以让我们的数据生成器产生一个字典来提供三个输入。我们还需要做一些整理,让字符级数据与单词级数据对齐:
def combined_data_generator(tweets, tokens, batch_size):
tweets = tweets.reset_index()
while True:
batch_idx = random.sample(range(len(tweets)), batch_size)
tweet_batch = tweets.iloc[batch_idx]
token_batch = tokens[batch_idx]
char_vec = np.zeros((batch_size, max_sequence_len, len(chars)))
token_vec = np.zeros((batch_size, max_num_tokens))
y = np.zeros((batch_size,))
it = enumerate(zip(token_batch, tweet_batch.iterrows()))
for row_idx, (token_row, (_, tweet_row)) in it:
y[row_idx] = emoji_to_idx[tweet_row['emoji']]
for ch_idx, ch in enumerate(tweet_row['text']):
char_vec[row_idx, ch_idx, char_to_idx[ch]] = 1
token_vec[row_idx, :] = token_row
yield {'char_cnn_input': char_vec,
'cnn_input': token_vec,
'lstm_input': token_vec}, y
然后我们可以使用以下方法训练模型:
BATCH_SIZE = 512
ensemble.fit_generator(
combined_data_generator(train_tweets, training_tokens, BATCH_SIZE),
epochs=20,
steps_per_epoch=len(train_tweets) / BATCH_SIZE,
verbose=2,
callbacks=[early]
)
组合模型或集成模型是将各种方法组合到一个模型中的好方法。在 Kaggle 等流行的机器学习竞赛中,获胜者几乎总是基于这种技术,这并非巧合。
与其保持模型几乎完全分离,然后在最后使用Average
层将它们连接起来,我们还可以更早地连接它们,例如通过连接每个模型的第一个密集层。事实上,这在某种程度上是我们对更复杂的 CNN 所做的,我们对小子网使用了不同的窗口大小,然后将它们连接起来得出最终结论.