欢迎回到NLP世界的介绍。如果你错过了NLP简介的第一部分,请单击此处:
https://towardsdatascience.com/the-bible-under-the-nlp-eye-part-1-416dbfd79444
今天我们将探索新的NLP工具,分析圣经:
- 主题建模:从文献(圣经书籍)到主题
- 词嵌入:如何获得嵌入,我们可以从这种方法中获得什么信息?
- 文本相似性:福音书彼此有多相似?我们可以使用什么技术来检测相似性,我们对文本相似性意味着什么?
在这里,你可以找到每个主题所需的所有包——更多的包将随文章一起提供。在这里,我定义了原始数据的预处理和清理代码
主题建模
数据科学家通常使用主题建模来从非结构化数据中获取洞察力,检索关键的信息(例如评论、医疗文档、机器信息、元数据)。
商业中有无数主题建模的例子,从总结评论到在客户电话中发现商业问题。特别是,有两种主要的主题建模策略:1)非负矩阵分解(NMF)和2)潜在狄利克雷分配(LDA)。
在本教程中,我们将使用LDA。LDA是在2003年的一篇论文中(是的,19年前!)由David Blei、吴恩达和Michael Jordan发表。
我会写一篇关于这项技术的公开文章,因为我喜欢它,但是,我很高兴你能抓住LDA工作的基本概念:可交换性。可交换性是一个简单的数学假设,1985年和1990年发表的论文。根据意大利的Guido De Finetti定理,任何可交换随机变量的集合都可以表示为混合分布。
简而言之,我们可以在不考虑文档中单词顺序的情况下创建主题模型。利用这个定理,我们可以通过混合分布来捕获重要的文档内统计结构。
让我们从旧约开始,通过清理和分组所有书籍,就像我们在第一个教程中所做的那样,让我们看到所有书籍中的主题。
下面显示了对旧约书进行LDA的工作流程。
def format_topics_sentences(ldamodel, corpus):
r"""This function associate to each review the dominant topic
Parameters
----------
lda_model: gensim lda_model
The current lda model calculated
corpus: gensim corpus
this is the corpus from the reviews
texts: list
list of words of each review
real_text: list
list of real comments
Return
------
"""
topics_df = pd.DataFrame()
# Get main topic in each document
for i, row in enumerate(ldamodel[corpus]): #from the corpus rebuild the reviews
row = sorted(row[0], key=lambda x: (x[1]), reverse=True)
# Get the Dominant topic, Perc Contribution and Keywords for each document
for j, (topic_num, prop_topic) in enumerate(row):
if j == 0: # => dominant topic
wp = ldamodel.show_topic(topic_num) #topic + weights
topic_keywords = ", ".join([word for word, prop in wp]) #topic keyword only
#prop_topic is the percentage of similarity of that topic
topics_df = topics_df.append(pd.Series([int(topic_num),\
round(prop_topic,2), topic_keywords]), ignore_index=True)
#round approximate the prop_topic to 2 decimals
else:
break
topics_df.columns = ['Dominant_Topic', 'Perc_Contribution', 'Topic_Keywords']
return(topics_df)
def run_lda(cleaned_comments, num_topics, chunksize):
r"""This is the main function which computes the LDA
Parameters
----------
cleaned_comments: "further_cleaning" in dataframe
comments: "comments" in dataframe
save_path: option whhere to save the output
num_topic: number of topics
chunksize: the chunk size of each comment
Return
------
lda_model: Gensim
LDA model
"""
#tokenize
data_words = []
for sentence in cleaned_comments:
data_words.append(simple_preprocess(str(sentence),deacc=True))#deacc remove punctuation
# Create Dictionary
id2word = Dictionary(data_words) #this create an index for each word
#e.g. id2word[0] = "allowed"
# Create Corpus
texts = data_words
# Term Document Frequency
corpus = [id2word.doc2bow(text) for text in texts]#bag of words
#corpus gives the frequency of a word in a document (a document == a single review)
# Build LDA model
#creation of lda with X topics and representation
print("Computing LDA with {} topics, {} chunksize...".format(num_topics, chunksize))
# gensim.models.ldamodel.LdaModel
lda_model = gensim.models.ldamodel.LdaModel(corpus=corpus,
id2word=id2word,
num_topics=num_topics,
random_state=42,
eval_every=100,
chunksize=chunksize,
passes=5,
iterations=400,
per_word_topics=True)
print("Writing classification onto csv file...")
df_topic_sents_keywords = format_topics_sentences(lda_model, corpus)
print("Topic Keywords")
print(df_topic_sents_keywords["Topic_Keywords"].unique())
print(f"Perplexity {lda_model.log_perplexity(corpus)}")
coherence_model_lda = CoherenceModel(model=lda_model, texts=data_words, dictionary=id2word, coherence='c_v')
coherence_lda = coherence_model_lda.get_coherence()
print(f"Coherence {coherence_lda}")
return lda_model, df_topic_sents_keywords, corpus
num_topics = [2, 3, 4, 5]
chunksizes = [20, 50, 100]
for num_topic in num_topics:
for chunksize in chunksizes:
print(f"!!!!!! Num Topic {num_topic} and chunksize {chunksize}")
lda_model, df_lda, corpus = run_lda(data,
num_topic,
chunksize )
首先,在第60行对每本书进行gensim.utils.simple_preprocess,将其视为唯一的字符串/句子。
其次,我们将创建一个字典,其中有一个单词索引映射(例如,在LDA算法中,允许的单词表示为0)。一旦映射了id2word,我们就可以创建一个“单词包”。语料库为每个文档返回每个单词的频率。最后,我们使用gensim通过gensim.models.ldamodel.ldamodel运行LDA算法
现在,让我们花一分钟看看我们是如何运行LDA的。LDA理论明确指出,我们需要给出我们期望的主题的输入数量。此外,我们还有一些其他的超参数,例如,块大小和每遍的迭代次数。
在这个例子中,我试图探索主题的数量和块的大小,以找到最佳主题。然而,请稍等,我们如何衡量成功的主题建模?我们有两个很好的指标可以帮助我们进行主题建模:困惑度和连贯性。
前一个度量是预测度量,它与模型交叉熵相关联。困惑评估模型在训练集上训练后预测测试集主题的能力。很难对这一得分给出正确的解释,通常越负面越好,但事实证明,困惑并没有一个清晰的人类解释。
第二个指标,连贯性,在这方面帮助我们,通过测量可解释主题的程度-从更人性化的意义上来说,通过测量主题中的单词与文档中的单词的相似程度。作为一条建议,除此之外,总是需要一点人性化的解释。
事实上,这应该是你可以获得的最佳结果:
Num Topic 4 and chunksize 20 Computing LDA with 4 topics, 20 chunksize...
Writing classification onto csv file...
Topic Keywords
['lord, saith, land, god, israel, people, shalt, king, man, day' 'lord, israel, king, god, said, son, house, people, juda, jerusalem' 'god, man, lord, things, heart, good, wisdom, men, fear, soul' 'king, great, jews, us, men, kings, daniel, kingdom, came, day']
正如你所看到的,前两个主题或多或少有着相同的含义,它们指的是应许之地,以色列或犹大人之家(或多或多是地理上的)。第三个需要一点解释,它可以指上帝的智慧以及人们应该如何害怕上帝,也可以指上帝和人类之间的相似性“上帝以自己的形象创造了人”。最终的结局是丹尼尔的故事,以及国王终有一天会到来的预言。
让我们为新约做同样的事情:
Num Topic 3 and chunksize 20 Computing LDA with 3 topics, 20 chunksize…
Writing classification onto csv file…
Topic Keywords
[‘said, jesus, man, god, things, come, therefore, lord, came, say’ ‘god, christ, also, things, jesus, may, lord, faith, sin, according’ ‘god, earth, great, seven, angel, beast, heaven, voice, throne, come’]
Perplexity -7.394623212885921 Coherence 0.4202057333015461
Num Topic 4 and chunksize 50 Computing LDA with 4 topics, 50 chunksize…
Writing classification onto csv file…
Topic Keywords
[‘said, jesus, god, man, lord, things, come, came, saying, say’ ‘god, christ, also, things, lord, jesus, man, may, faith, according’ ‘god, earth, great, seven, come, heaven, angel, saying, things, beast’
‘god, christ, sin, law, faith, jesus, son, also, spirit, things’]
Perplexity -7.347456308975332 Coherence 0.3708218577493271
Num Topic 5 and chunksize 50 Computing LDA with 5 topics, 50 chunksize…
Writing classification onto csv file…
Topic Keywords
[‘said, jesus, god, man, lord, things, come, came, saying, also’ ‘god, christ, also, things, lord, jesus, man, may, faith, according’ ‘god, earth, great, seven, come, heaven, angel, saying, things, beast’
‘god, christ, sin, law, spirit, jesus, faith, also, son, world’ ‘jesus, things, god, christ, may, men, saviour, good, also, lord’]
Perplexity -7.353096888524062 Coherence 0.3734882570283872
正如你所看到的,许多主题相互重叠。我个人认为,3个主题足以区分所有新约主题。前者said, jesus,man, god, things, come, therefore, lord, came say是指耶稣的比喻;第二个主题god, christ, also, things, Jesus, may, lord, faith, sin, according可能指的是对上帝的信仰和耶稣为人类的罪而牺牲;最后一个主题是指god, earth, great, seven, angel, beast, heaven, voice, throne, come
总而言之,主题建模是一种很好的技术,可以理解文档中引入的关键词和主题。我们不能用一种简单的方式来衡量话题的正确性,但我们必须采用困惑和连贯性,无论如何,如果没有很好的人类解读,这是不够的。
从《旧约》和《新约》衍生出三个主要主题,从应许之地到启示录。我们能做更多的事情来整合主题建模吗?当然,我们可以在词嵌入上运行主题建模。
词嵌入
现在让我们转到更复杂的事情:词嵌入。许多(显然)单词和文章都花在了词嵌入上。
简言之,词嵌入允许我们将多维单词、句子和文档简化为人类可解释的含义和计算有效的向量。一个向量可以有很多维度,但当所有的单词都投影到向量上时,就可以将它们分为不同的类,从而解决并显著简化真正的海量问题,并在单词之间找到有趣的关系。
对于词嵌入,我们需要以下附加包:
from sklearn.manifold import TSNE
from collections import Counter
from six.moves import cPickle
import gensim.models.word2vec as w2v
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import multiprocessing
import os
import sys
import io
import re
import json
import nltk
nltk.download('punkt')
然后,我们将计算嵌入并创建一个w2v文件,以保存gensim.w2v.Word2Vec中的二进制词嵌入
# use the documents' list as a column in a dataframe
df = pd.DataFrame(data, columns=["text"])
def get_word2vec(text):
r"""
Parameters
-----------
text: str, text from dataframe, df['text'].tolist()"""
num_workers = multiprocessing.cpu_count()
num_features = 200# tune this bit
epoch_count = 10
tokens = [nltk.word_tokenize(sentence) for sentence in text]
sentence_count = len(text)
word2vec = None
word2vec = w2v.Word2Vec(sg=1,
seed=1,
workers=num_workers,
size=num_features,
min_count=min_frequency_val,
window=5,
sample=0)
print("Building vocab...")
word2vec.build_vocab(tokens)
print("Word2Vec vocabulary length:", len(word2vec.wv.vocab))
print("Training...")
word2vec.train(tokens, total_examples=sentence_count, epochs=epoch_count)
print("Saving model...")
word2vec.save(w2v_file)
return word2vec
# save results in an analysis folder
save_dir = "analysis"
if not os.path.exists(save_dir):
os.makedirs(save_dir)
# set up a minimal frequency value
min_frequency_val=6
# save the word2vec in a file
w2v_file = os.path.join(save_dir, "word_vectors.w2v")
word2vec = get_word2vec(df['text'].tolist())
现在,我们想可视化这个多维向量。通常的方法是使用t分布随机邻居嵌入,这是一种非线性降维技术。下面的代码将为我们提供2D中的词嵌入向量:
def get_word_frequencies(text):
r""" This function return a Counter with the most common words
in a given text
Parameters
----------
text: df['text'].tolist()
Return
------
freq: Counter, most common words with their freqs
"""
frequencies = Counter()
tokens = [nltk.word_tokenize(sentence) for sentence in text]
for token in tokens:
for word in token:
frequencies[word] += 1
freq = frequencies.most_common()
return freq
def most_similar(input_word, num_similar):
r""" This function uses word2vec to find the most
num_similar similar words wrt a given
input_word
Parameters
-----------
input_word: str, input word
num_similar: int, how many similar words we want to get
Return
------
output: list, input word and found words
"""
sim = word2vec.wv.most_similar(input_word, topn=num_similar)
output = []
found = []
for item in sim:
w, n = item
found.append(w)
output = [input_word, found]
return output
def calculate_t_sne(word2vec):
r""" Main function to copmute the t-sne representation
of the computed word2vec
"""
vocab = word2vec.wv.vocab.keys()
vocab_len = len(vocab)
dim0 = word2vec.wv[vocab].shape[1]
arr = np.empty((0, dim0), dtype='f')
labels = []
vectors_file = os.path.join(save_dir, "vocab_vectors.npy")
labels_file = os.path.join(save_dir, "labels.json")
print("Creating an array of vectors for each word in the vocab")
for count, word in enumerate(vocab):
if count % 50 == 0:
print_progress(count, vocab_len)
w_vec = word2vec[word]
labels.append(word)
arr = np.append(arr, np.array([w_vec]), axis=0)
save_bin(arr, vectors_file)
save_json(labels, labels_file)
x_coords = None
y_coords = None
x_c_filename = os.path.join(save_dir, "x_coords.npy")
y_c_filename = os.path.join(save_dir, "y_coords.npy")
print("Computing T-SNE for array of length: " + str(len(arr)))
tsne = TSNE(n_components=2, random_state=1, verbose=1)
np.set_printoptions(suppress=True)
Y = tsne.fit_transform(arr)
x_coords = Y[:, 0]
y_coords = Y[:, 1]
print("Saving coords.")
save_bin(x_coords, x_c_filename)
save_bin(y_coords, y_c_filename)
return x_coords, y_coords, labels, arr
def t_sne_scatterplot(word):
r""" Function to plot the t-sne result for a given word
Parameters
----------
word: list, given word we want to plot the w2v-tsne plot + its neighbours
"""
vocab = word2vec.wv.vocab.keys()
vocab_len = len(vocab)
vocab_elems = [key for key in vocab]
dim0 = word2vec.wv[vocab_elems[0]].shape[0]
arr = np.empty((0, dim0), dtype='f')
w_labels = [word]
# check all the similar words around
nearby = word2vec.wv.similar_by_word(word, topn=num_similar)
arr = np.append(arr, np.array([word2vec[word]]), axis=0)
for n in nearby:
w_vec = word2vec[n[0]]
w_labels.append(n[0])
arr = np.append(arr, np.array([w_vec]), axis=0)
tsne = TSNE(n_components=2, random_state=1)
np.set_printoptions(suppress=True)
Y = tsne.fit_transform(arr)
x_coords = Y[:, 0]
y_coords = Y[:, 1]
plt.rc("font", size=16)
plt.figure(figsize=(16, 12), dpi=80)
plt.scatter(x_coords[0], y_coords[0], s=800, marker="o", color="blue")
plt.scatter(x_coords[1:], y_coords[1:], s=200, marker="o", color="red")
for label, x, y in zip(w_labels, x_coords, y_coords):
plt.annotate(label.upper(), xy=(x, y), xytext=(0, 0), textcoords='offset points')
plt.xlim(x_coords.min()-50, x_coords.max()+50)
plt.ylim(y_coords.min()-50, y_coords.max()+50)
filename = os.path.join(plot_dir, word + "_tsne.png")
plt.savefig(filename)
plt.close()
def test_word2vec(test_words):
r""" Function to check if a test word exists within our vocabulary
and return the word along with its most similar, thorugh word
embeddings
Parameters
----------
test_words: str, given word to check
Return
------
output: list, input word and associated words
"""
vocab = word2vec.wv.vocab.keys()
vocab_len = len(vocab)
output = []
associations = {}
test_items = test_words
for count, word in enumerate(test_items):
if word in vocab:
print("[" + str(count+1) + "] Testing: " + word)
if word not in associations:
associations[word] = []
similar = most_similar(word, num_similar)
t_sne_scatterplot(word)
output.append(similar)
for s in similar[1]:
if s not in associations[word]:
associations[word].append(s)
else:
print("Word " + word + " not in vocab")
filename = os.path.join(save_dir, "word2vec_test.json")
save_json(output, filename)
filename = os.path.join(save_dir, "associations.json")
save_json(associations, filename)
filename = os.path.join(save_dir, "associations.csv")
handle = io.open(filename, "w", encoding="utf-8")
handle.write(u"Source,Target\n")
for w, sim in associations.items():
for s in sim:
handle.write(w + u"," + s + u"\n")
return output
def show_cluster_locations(results, labels, x_coords, y_coords):
r""" function to retrieve the cluster location from t-sne
Parameters
----------
results: list, word and its neighbours
labels: words
x_coords, y_coords: float, x-y coordinates 2D plane
"""
for item in results:
name = item[0]
print("Plotting graph for " + name)
similar = item[1]
in_set_x = []
in_set_y = []
out_set_x = []
out_set_y = []
name_x = 0
name_y = 0
for count, word in enumerate(labels):
xc = x_coords[count]
yc = y_coords[count]
if word == name:
name_x = xc
name_y = yc
elif word in similar:
in_set_x.append(xc)
in_set_y.append(yc)
else:
out_set_x.append(xc)
out_set_y.append(yc)
plt.figure(figsize=(16, 12), dpi=80)
plt.scatter(name_x, name_y, s=400, marker="o", c="blue")
plt.scatter(in_set_x, in_set_y, s=80, marker="o", c="red")
plt.scatter(out_set_x, out_set_y, s=8, marker=".", c="black")
filename = os.path.join(big_plot_dir, name + "_tsne.png")
plt.savefig(filename)
plt.close()
x_coords, y_coords, labels, arr = calculate_t_sne(word2vec)
# and let's save the t-sne plots with the words clusters
frequencies = get_word_frequencies(df['text'].tolist())
# check the first 50 most frequent words and see if they're in the w2v
for item in frequencies[:50]:
test_words.append(item[0])
results = test_word2vec(test_words)
# and once we have all the word + neighbors let's see how the t-sne has grouped them
show_cluster_locations(results, labels, x_coords, y_coords)
代码很长,所以我们现在做的是:
- 首先,我们使用calculate_t_sne通过t-sne减少维度,从而产生x_cords、y_coords和标签(这是单词)
- 我们使用Counter().most_common()计算单词的频率
- 对于前50个最常出现的单词,我们将通过most_simple检查这些单词如何与word2vec词汇表中的其他单词向量相关联。最后,我们返回一系列列表,列出50个最常用的单词中的每一个,并为每一个单词添加相关单词。
- 在这一步之后,我们将这一系列列表转换为t-s空间中的x和y坐标,并绘制最终结果。
在我看来,下图显示了word2vec的全部力量
下面显示了“death”一词的另一个独特的嵌入词。我们可以看到,life正处于相反的位置,而我们之间有复仇、犯罪、疾病、危险和困惑
图6是我们可以从word2vec得到的信息的最后一个例子,evil在相反的方向上,我们会有hope,但有趣的是,我们可以看到整个圣经中邪恶是如何联系在一起的。邪恶是错误的,它与疾病、邪恶和虚荣有关。
总之,词嵌入可以让你在单词之间隐藏关联。在实际情况下,场景词可以是评论、产品、爬取的数据,你可以通过嵌入建立重要的联系。此外,词嵌入可以并且应该与主题建模一起使用,以进一步利用单词关联来寻找更多可解释的主题。
文本相似性
圣经中讨论最多的一个方面是书籍的重叠和相似性,尤其是福音书。事实证明,马可福音76%的内容是在马太福音和路加福音中分享的。此外,马太和路加分享了大约50%的内容。有点不同的是《约翰福音》,它写于近100年后,写作风格与前面的福音书相去甚远。
你可以尝试的文本相似性的第一种方法是运行词频-逆文档频率或TF-IDF。在这种方法中,我们将考虑整个语料库中的单词频率,而不是单个文档(例如,整个圣经而不是一本书)。关键的一点是,在任何地方经常出现的单词的意义都很低,而出现频率较低的单词可能有意义。使用scikit-learn,TF-IDF的实现很简单,如图7所示
def create_heatmap(similarity, cmap = "YlGnBu"):
df = pd.DataFrame(similarity)
df.columns = ['john', 'luke','mark', 'matt'] #ohn 0 mark 2 matt 3 luke 1
df.index = ['john', 'luke','mark', 'matt']
fig, ax = plt.subplots(figsize=(5,5))
sns.heatmap(df, cmap=cmap)
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfVectorizer
import seaborn as sns
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(data)
arr = X.toarray()
create_heatmap(cosine_similarity(arr))
这里的数据是一个pandas数据帧,其中每一列都是福音。文本通过TfidfVectorizer进行处理。最后,通过cosine_similarityig计算相似度。
记住,当你想测量向量之间的相似性时,余弦相似性是最好的方法。下图显示了四部福音书的相似性结果。正如你所看到的,剧情强烈地反映了福音书之间的相似性有多强,加上一点计算能力,我们已经对《圣经》的四本主要书籍有了很好的看法。
这很好,但我们可以尝试做更多,分析每本书的上下文内容。sentence_transformers可以实现更精细的方法。
这里我们可以引用大量的模型,其中最著名的是BERT和句子BERT(SBERT),它由暹罗网络组成,用于导出句子嵌入。这里我们不仅要看文本的相似性,还要看上下文的相似性。
下面显示了实现,由于sentence_transforms包包含了我们想要的所有相关transformer,所以实现非常简单。
我们选择stsb roberta large,并对我们拥有的输入数据进行编码。最后,正如我们之前所做的,相似性是用余弦相似性来衡量的。
!pip install sentence_transformers
from sentence_transformers import SentenceTransformer, util
# use roberta
model = SentenceTransformer('stsb-roberta-large')
def create_heatmap(similarity, cmap = "YlGnBu"):
df = pd.DataFrame(similarity)
df.columns = ['john', 'luke','mark', 'matt'] #ohn 0 mark 2 matt 3 luke 1
df.index = ['john', 'luke','mark', 'matt']
fig, ax = plt.subplots(figsize=(5,5))
sns.heatmap(df, cmap=cmap)
# encode the input text
embeddings = model.encode(data, convert_to_tensor=True)
similarity = []
for i in range(len(data)):
row = []
for j in range(len(data)):
row.append(util.pytorch_cos_sim(embeddings[i], embeddings[j]).item())
similarity.append(row)
create_heatmap(similarity)
下面显示了获得的结果!在这里,我们获得了新的有趣信息。我们知道《约翰福音》和《路加福音》的基调与马太福音和马可福音相去甚远,马太福音是前两部福音书。因此,在语境层面上,这两本福音书之间可能有更多的相似之处,即其他福音书中没有出现的词语和概念。看到所有其他福音书在上下文层面上都大不相同,真的很有趣。
结论
这是我们关于NLP与圣经的两部分教程的结尾。让我们回顾一下我们今天所做的:
- 我们了解了什么是主题建模,以及LDA建模的关键点是什么:可交换性
- 我们可以做更多的事情来理解单词的含义,并将主题建模中的信息与词嵌入相结合
- 我们可以使用一种简单的技术(如TF-IDF)来测量文档之间的文本相似性,或者,如果我们想在上下文级别上研究相似性,我们可以使用奇妙的sentence_transforms库并启动一个transformer来测量文本相似性。
感谢阅读!