介绍
博客在数据科学界很受欢迎已经不是什么秘密了。通过这种方式,该领域反映了其在开源运动中的根源。在找到问题的创新解决方案之后,数据科学家似乎没有什么比写它更感兴趣了。数据科学界的博客是一个双赢的局面,作家从曝光中获益,读者从获得的知识中获益。
在本教程中,将使用主题建模来表征与数据科学相关的媒体文章的内容,然后使用主题模型输出来构建基于内容的推荐器。作为语料库,将使用Kaggle数据集中文文章(包含内容),其中包含大约70,000个已被标记为数据科学,机器学习,AI或人工智能的中等文章。这是一个很好的数据集,因为它除了文章全文外还包含大量信息:拍手数量,作者,网址等。数据集包含最近于2018年10月发布的文章。这意味着推荐人不会建议最新的帖子,但这没关系。
https://www.kaggle.com/aiswaryaramachandran/medium-articles-with-content
加载数据
首先导入库,将数据集加载到pandas数据框中,然后查看前几行。
import numpy as np
import pandas as pd
import re
import string
from sklearn.decomposition import NMF
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
import gensim
from gensim.parsing.preprocessing import STOPWORDS
from gensim import corpora, models
from gensim.utils import simple_preprocess
from nltk.stem.porter import PorterStemmer
medium = pd.read_csv(‘Medium_AggregatedData.csv’)
medium.head()
看起来未处理的数据集包含大量冗余信息。事实上,分配给文章的每个标签都有一行,因此每篇文章最多5行。通过压缩标签信息然后消除重复行来解决这个问题。为了进一步减小数据集的大小并确保提供高质量的建议,还要删除不是用英语写的文章和少于25个文章的文章。最后,将删除所有未使用的列。
# Filter articles
medium = medium[medium['language'] == 'en']
medium = medium[medium['totalClapCount'] >= 25]
def findTags(title):
'''
Function extracts tags for an input title
'''
rows = medium[medium['title'] == title]
tags = list(rows['tag_name'].values)
return tags
# Get all the titles
titles = medium['title'].unique()
tag_dict = {'title': [], 'tags': []} # Dictionary to store tags
for title in titles:
tag_dict['title'].append(title)
tag_dict['tags'].append(findTags(title))
tag_df = pd.DataFrame(tag_dict) # Dictionary to data frame
# Now that tag data is extracted the duplicate rows can be dropped
medium = medium.drop_duplicates(subset = 'title', keep = 'first')
def addTags(title):
'''
Adds tags back into medium data frame as a list
'''
try:
tags = list(tag_df[tag_df['title'] == title]['tags'])[0]
except:
# If there's an error assume no tags
tags = np.NaN
return tags
# Apply addTags
medium['allTags'] = medium['title'].apply(addTags)
# Keep only the columns we're interested in for this project
keep_cols = ['title', 'url', 'allTags', 'readingTime',
'author', 'text']
medium = medium[keep_cols]
# Drop row with null title
null_title = medium[medium['title'].isna()].index
medium.drop(index = null_title, inplace = True)
medium.reset_index(drop = True, inplace = True)
print(medium.shape)
medium.head()
现在,数据集已减少到仅仅24,576行,并且标记信息已保留在“allTags”列中。这将更容易与未来合作。
文字清理
现在将注意力转移到预处理文章文本以准备主题建模。首先将删除链接,非字母数字字符和标点符号。还会将所有字符转换为小写字母。
def clean_text(text):
'''
Eliminates links, non alphanumerics, and punctuation.
Returns lower case text.
'''
# Remove links
text = re.sub('(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+
\.[\w/\-?=%.]+','', text)
# Remove non-alphanumerics
text = re.sub('\w*\d\w*', ' ', text)
# Remove punctuation and lowercase
text = re.sub('[%s]' % re.escape(string.punctuation),
' ', text.lower())
# Remove newline characters
text = text.replace('\n', ' ')
return text
medium['text'] = medium['text'].apply(clean_text)
接下来在预处理流程中是消除停用词,这些词非常常见且没有信息。对于许多NLP任务,这些词是噪音,混淆了试图找到的任何信号。标准英语停用词的几个例子是'the','is'和'you'。此外,考虑特定于域的停用词通常也很重要。对于这个项目,将从Gensim预定义的一组停用词开始,然后添加数据科学特定的停用词和由预处理步骤生成的一些单词片段。
stop_list = STOPWORDS.union(set(['data', 'ai', 'learning', 'time', 'machine', 'like', 'use', 'new', 'intelligence', 'need', "it's", 'way', 'artificial', 'based', 'want', 'know', 'learn', "don't", 'things', 'lot', "let's", 'model', 'input', 'output', 'train', 'training', 'trained', 'it', 'we', 'don', 'you', 'ce', 'hasn', 'sa', 'do', 'som', 'can']))
# Remove stopwords
def remove_stopwords(text):
clean_text = []
for word in text.split(' '):
if word not in stop_list and (len(word) > 2):
clean_text.append(word)
return ' '.join(clean_text)
medium['text'] = medium['text'].apply(remove_stopwords)
在语料库上运行单词计数(删除标准停用词后)可以快速识别一些更明显的特定于域的停用词,但通常这些停用词列表需要通过反复试验来完善。
作为最后的预处理步骤,将一个词干分析器应用于文档,以将各种单词时态和变形转换为标准化词干。这将产生一些出现屠杀的词干(即图像→图像和商业→商业),但是人类通常很容易识别真正的根。
# Apply stemmer to processedText
stemmer = PorterStemmer()
def stem_text(text):
word_list = []
for word in text.split(' '):
word_list.append(stemmer.stem(word))
return ' '.join(word_list)
medium['text'] = medium['text'].apply(stem_text)
已经准备好继续进行主题建模,但是首先将当前数据框保存到csv文件中。
medium.to_csv('pre-processed.csv')
主题建模
通过预处理完成,终于可以通过主题建模获得一些乐趣。主题建模的想法是将文档转换为稀疏的单词向量,然后应用降维技术来找到有意义的单词分组。为此将使用不同的方法构建许多模型并比较结果。将寻找能够产生最清晰,最具凝聚力和差异化主题的模型。这是无监督学习的领域,对结果的评估是主观的,需要良好的人类判断。
构建主题模型的第一步是将文档转换为单词向量。有两种常用的方法,BOW(词袋)和TFIDF(术语频率,逆文档频率)。BOW只计算单词出现在文档中的次数。如果“总统”一词在文档中出现5次,那么将在文档的稀疏单词向量的相应插槽中转换为数字5。
另一方面,TFIDF的运作假设每个文档中出现的单词对任何一个单独的文档都不那么重要。例如,考虑与2020年总统选举有关的文件集。显然,“总统”这个词几乎会出现在关于这个主题的每篇文章中,而“总统”对于分析这种背景下的任何单个文档来说都不是一个特别有用的词。
为了简洁起见,将重点关注TFIDF主题模型实现,除了LDA算法仅适用于BOW的情况。根据经验,TFIDF通常可以更好地提取清晰,有凝聚力和差异化的主题。为了开始,将文档语料库转换为TFIDF稀疏向量表示,并将SVD(单值分解)应用于稀疏语料库矩阵。
vectorizer = TfidfVectorizer(stop_words = stop_list,
ngram_range = (1,1))
doc_word = vectorizer.fit_transform(medium['text'])
svd = TruncatedSVD(8)
docs_svd = svd.fit_transform(doc_word)
这将从语料库中提取8个主题(8是该语料库的最佳主题数,但尝试使用不同的数字进行试验)并将文档转换为8维向量,这些向量表示该文档中每个主题的存在。现在编写一个函数来打印每个主题中最突出的单词,以便可以评估SVD算法的执行情况。
def display_topics(model, feature_names, no_top_words, no_top_topics, topic_names=None):
count = 0
for ix, topic in enumerate(model.components_):
if count == no_top_topics:
break
if not topic_names or not topic_names[ix]:
print("\nTopic ", (ix + 1))
else:
print("\nTopic: '",topic_names[ix],"'")
print(", ".join([feature_names[i]
for i in topic.argsort()[:-no_top_words -
1:-1]]))
count += 1
display_topics(svd, vectorizer.get_feature_names(), 15, 8)
不错,但看看能否做得更好。下一个要尝试的算法是NMF(非负矩阵分解)。该算法与SVD非常相似。有时它会产生更好的结果,有时会更糟。现在就看看吧。
nmf = NMF(8)
docs_nmf = nmf.fit_transform(doc_word)
display_topics(nmf, vectorizer.get_feature_names(), 15, 8)
这看起来很不错。这些主题比使用SVD生成的主题更有区别。
最后,试试LDA(潜在的dirichlet分配)。该算法最近变得非常流行用于主题建模,并且被许多人认为是最先进的。也就是说,评估仍然是非常主观的,并且结果不能保证比SVD或NMF更好。要实现LDA,将使用Gensim库,这意味着代码看起来会有所不同。
tokenized_docs = medium['text'].apply(simple_preprocess)
dictionary = gensim.corpora.Dictionary(tokenized_docs)
dictionary.filter_extremes(no_below=15, no_above=0.5, keep_n=100000)
corpus = [dictionary.doc2bow(doc) for doc in tokenized_docs]
# Workers = 4 activates all four cores of my CPU,
lda = models.LdaMulticore(corpus=corpus, num_topics=8,
id2word=dictionary, passes=10,
workers = 4)
lda.print_topics()
这些主题非常好。也就是说,认为用NMF获得的那些稍微明显一点。对于基于内容的推荐人,主题之间的区别至关重要。这使得推荐者能够将文章与用户的品味相匹配。考虑到上述情况,继续使用NMF主题。
为了继续,命名NMF主题,并将文档主题向量连接回包含文章元数据其余部分的数据框。然后,将该数据帧保存到自己的csv文件中,以便以后轻松访问。
# Define column names for dataframe
column_names = ['title', 'url', 'allTags', 'readingTime', 'author',
'Tech', 'Modeling', 'Chatbots', 'Deep Learning',
'Coding', 'Business', 'Careers', 'NLP', 'sum']
# Create topic sum for each article
# Later remove all articles with sum 0
topic_sum = pd.DataFrame(np.sum(docs_nmf, axis = 1))
# Turn our docs_nmf array into a data frame
doc_topic_df = pd.DataFrame(data = docs_nmf)
# Merge all of our article metadata and name columns
doc_topic_df = pd.concat([medium[['title', 'url', 'allTags',
'readingTime', 'author']], doc_topic_df,
topic_sum], axis = 1)
doc_topic_df.columns = column_names
# Remove articles with topic sum = 0, then drop sum column
doc_topic_df = doc_topic_df[doc_topic_df['sum'] != 0]
doc_topic_df.drop(columns = 'sum', inplace = True)
# Reset index then save
doc_topic_df.reset_index(drop = True, inplace = True)
doc_topic_df.to_csv('tfidf_nmf_8topics.csv', index = False)
构建推荐引擎
最后是构建推荐器后端的时候了。作为输入,推荐者将分配主题; 然后它会找到一篇与该发行版非常匹配的文章。为了多样化,引入一点随机性也是一个好主意。这将允许系统从更多数量的文章中进行选择,同时仍然产生高质量的推荐。
在实践中,计算输入分布与任何文章之间相似性的简单方法是使用余弦距离。当两个矢量指向相同方向并且与矢量的比例不变时,余弦距离最大化。后者属性相当不错,因为它允许忽略矢量缩放,对于欧几里德距离也是如此。
至于随机性,这可以通过向输入添加随机8维向量来合并。为了稳定随机性的大小,应该将该随机向量缩放到用户输入向量的距离。
最后要考虑的事情。使用for循环计算输入和每个可能输出之间的余弦距离将非常慢。显然不能让用户等待30秒的推荐。解决方案是矢量化,或者换句话说,使用线性代数并行化计算。将在Numpy中使用矩阵和向量运算来完成此操作。这将使代码能够更快地运行数量级并几乎立即生成建议。看看这一切是如何运作的。
topic_names = ['Tech', 'Modeling', 'Chatbots', 'Deep Learning',
'Coding', 'Business', 'Careers', 'NLP']
topic_array = np.array(doc_topic_df[topic_names])
norms = np.linalg.norm(topic_array, axis = 1)
def compute_dists(top_vec, topic_array):
'''
Returns cosine distances for top_vec compared to every article
'''
dots = np.matmul(topic_array, top_vec)
input_norm = np.linalg.norm(top_vec)
co_dists = dots / (input_norm * norms)
return co_dists
def produce_rec(top_vec, topic_array, doc_topic_df, rand = 15):
'''
Produces a recommendation based on cosine distance.
rand controls magnitude of randomness.
'''
top_vec = top_vec + np.random.rand(8,)/
(np.linalg.norm(top_vec)) * rand
co_dists = compute_dists(top_vec, topic_array)
return doc_topic_df.loc[np.argmax(co_dists)]
创建一些示例用户输入,看看会出现什么。
tech = 5
modeling = 5
chatbots = 0
deep = 0
coding = 0
business = 5
careers = 0
nlp = 0
top_vec = np.array([tech, modeling, chatbots, deep, coding, business, careers, nlp])
rec = produce_rec(top_vec, topic_array, doc_topic_df)
Rec
有效。推荐人根据输入产生了一篇有趣的文章,还得到了一大堆相关的元数据。
结论
讨论了文本预处理,主题建模以及使用主题来构建推荐引擎。
这个项目的笔记本托管在Github上。
https://github.com/alexmuhr/medium-recommender-notebook.git