万字长文慎入,无需训练 RNN,写个 AI 来讲故事

By 超神经

内容概要:让 AI 讲述一个爱听的故事,需要怎样操作?本文将手把手的教你,如何从头搭建一个 AI 系统,让它讲出合你心意的电影故事。

关键词:自然语言处理  文本生成 

本文转载自「AI科技大本营」ID:rgznai100

作者 | Andre Ye / 译者 | 弯月

在本文中,将向你演示如何编写一个 AI,可以根据个人喜好来给我们讲故事,为沉闷的生活增添一份乐趣。

本文将分为以下几个部分:

1.蓝图:概述整个项目及其构成部分。

2.程序演示:在完成编写代码的工作后,作为预览演示系统的功能。

3.数据加载和清理:加载数据并准备好进行处理。

4.寻找最具有代表性的情节:该项目的第一部分,使用 K-Means 选择用户最感兴趣的情节。

5.总结图:使用基于图表的总结来获取每个情节的摘要,这是 UI 的组成部分。

6.推荐引擎:使用简单的预测式机器学习模型推荐新故事。

7.综合所有组件:编写能够将所有组件结合在一起的生态系统结构。

 

蓝图:构建快速可用的讲故事 AI

如何用简单高效的方式来训练递归神经网络或其他的生成式方法?以我从事文本生成工作的经验来看,训练要么需要花费很长很长的时间,要么就会出现过度拟合数据,导致无法完成「原始文本生成」的目标。

另外,训练一个性能良好的模型所需的时间超过 8 个小时,而训练深度学习模型最有效的免费平台 Kaggle ,最多也只能免费运行 8 小时。

因此我想创建一个快速、通用且每个人都可用的项目。让 AI 无需训练 RNN 或生成模型,只需从「故事数据库」中搜索人为创建的内容,就可找到用户喜欢的故事。

至于「故事数据库」,这里使用了 Kaggle 上的 Wikipedia 电影情节数据集。其中包含了多种类型、国家和时代的 3.5 万个电影故事。

数据内容包含电影的发行年份、标题、国家、类型和剧情文字说明。

确定好了数据之后,接下来需要设计一个粗略的大纲/蓝图。

1.这个程序先输出五个特性鲜明的故事的概要(这些故事的要点是可以区分用户的口味。)

2.用户对其进行评分,判断他们是喜欢、不喜欢还是持中立态度。

3.程序接收用户对这五个故事的喜好程度,并输出完整故事的摘要。如果用户感兴趣,则输出完整的故事,并在结束时要求用户提供反馈。模型将从实时反馈中学习并改进(强化学习系统)。

这里选择五个左右最有代表性的故事,是为了让模型在有限的数据量下获得尽可能多的信息。

 

系统演示:AI 如何知晓你的口味

首先,程序会提供每个簇中具有代表性的三个故事,要求你针对三个故事提供反馈。

在回答完前三个入门问题,模型对你的喜好会进行大致评估,然后开始生成符合个人口味的新故事。

过程会询问你的意见,如果对某个故事片段感兴趣,AI 会输出整个故事供你阅读。如果不喜欢则会输出新的故事。

而你的反馈(你是否喜欢故事)会添加到训练数据,以改善模型的推荐机制。

如果你喜欢某个故事的片段,并给出了「1」作为响应,那么模型会朝着这个方向,推荐更多同类的故事。

这个程序就像「蒙特卡洛树搜索」一样,朝着优化奖励的方向发展,并在偏离太远(与你喜欢的故事类型相距太远)时回退,从而优化阅读体验。

 

准备工作:数据加载和清理

熟悉了上面的流程后,我们通过 pandas 的 load_csv 来加载数据。

import pandas as pddata = pd.read_csv('/kaggle/input/wikipedia-movie-plots/wiki_movie_plots_deduped.csv')data.head()

数据集中的字段包括发行年份、电影名称、国家、导演、演员、类型、该电影在 Wikipedia 页面上的 URL 以及剧情的文字说明。

对于推荐算法或聚类方法来说,演和演员阵容的分类太多(共有 12593 个导演和 32182 演员),对结果收益不大,这里选择去掉它们。

电影类型的数量相对较少——100 多部电影的分类只有 30 多个,而且这代表了超过 80% 的电影(其他类型归类为「其他」)。

data.drop(['Director','Cast'],axis=1,inplace=True)

遇到的另一个问题引用中的括号去除,比如 Wikipedia 的引用来源编号(例如 [3] )。

"Grace Roberts (played by Lea Leland), marries rancher Edward Smith, who is revealed to be a neglectful, vice-ridden spouse. They have a daughter, Vivian. Dr. Franklin (Leonid Samoloff) whisks Grace away from this unhappy life, and they move to New York under aliases, pretending to be married (since surely Smith would not agree to a divorce). Grace and Franklin have a son, Walter (Milton S. Gould). Vivian gets sick, however, and Grace and Franklin return to save her. Somehow this reunion, as Smith had assumed Grace to be dead, causes the death of Franklin. This plot device frees Grace to return to her father's farm with both children.[1]"

对于上述字符串,我们需要删除[1]。最简单的解决方案是创建一个带有每个括号值([1],[2],[3],…,[98],[99])的列表,然后从字符串中删除列表中存在的每个值。但前提是要确保每篇文章的引用都不会超过 99 条。

也可以通过混乱的字符串索引或拆分来解决这个问题。

blacklist = []for i in range(100):    blacklist.append('['+str(i)+']')

这段代码创建了 blacklist,列表里包含了我们不想要的引用标记。

def remove_brackets(string):    for item in blacklist:        string = string.replace(item,'')    return string

接下来,我们可以使用 blacklist 创建一个函数 remove_brackets,然后应用到数据的每一列。

data['Plot'] = data['Plot'].apply(remove_brackets)

至此,基本的数据清理工作就完成了。

 

总结故事情节:提炼出要点

讲故事 AI 的一个关键能力是总结故事情节。由于一般的故事读起来都太长,因此具有总结故事的能力,可方便用户选择是否继续阅读。

这里使用当前流行的方法:基于图的摘要算法。首先创建文档单元图,然后选择具有适用于此场景的 PageRank 版本的节点。Google 原始的 PageRank 版本就是用类似方式查找网页节点。

PageRank 算法会计算图中的节点「中心」,以此衡量句子中相关信息的内容。该图的构造使用了词袋特征序列和基于余弦相似度的边缘权重。

我们将使用 gensim 库来总结长文本,它的实现方法如下:

import gensimstring = '''The PageRank algorithm outputs a probability distribution used to represent the likelihood that a person randomly clicking on links will arrive at any particular page. PageRank can be calculated for collections of documents of any size. It is assumed in several research papers that the distribution is evenly divided among all documents in the collection at the beginning of the computational process. The PageRank computations require several passes, called “iterations”, through the collection to adjust approximate PageRank values to more closely reflect the theoretical true value.Assume a small universe of four web pages: A, B, C and D. Links from a page to itself, or multiple outbound links from one single page to another single page, are ignored. PageRank is initialized to the same value for all pages. In the original form of PageRank, the sum of PageRank over all pages was the total number of pages on the web at that time, so each page in this example would have an initial value of 1. However, later versions of PageRank, and the remainder of this p, assume a probability distribution between 0 and 1. Hence the initial value for each page in this example is 0.25.The PageRank transferred from a given page to the targets of its outbound links upon the next iteration is divided equally among all outbound links.If the only links in the system were from pages B, C, and D to A, each link would transfer 0.25 PageRank to A upon the next iteration, for a total of 0.75.Suppose instead that page B had a link to pages C and A, page C had a link to page A, and page D had links to all three pages. Thus, upon the first iteration, page B would transfer half of its existing value, or 0.125, to page A and the other half, or 0.125, to page C. Page C would transfer all of its existing value, 0.25, to the only page it links to, A. Since D had three outbound links, it would transfer one third of its existing value, or approximately 0.083, to A. At the completion of this iteration, page A will have a PageRank of approximately 0.458.In other words, the PageRank conferred by an outbound link is equal to the document’s own PageRank score divided by the number of outbound links L( ). In the general case, the PageRank value for any page u can be expressed as: i.e. the PageRank value for a page u is dependent on the PageRank values for each page v contained in the set Bu (the set containing all pages linking to page u), divided by the number L(v) of links from page v. The algorithm involves a damping factor for the calculation of the pagerank. It is like the income tax which the govt extracts from one despite paying him itself.'''print(gensim.summarization.summarize(string))

输出:

In the original form of PageRank, the sum of PageRank over all pages was the total number of pages on the web at that time, so each page in this example would have an initial value of 1. The PageRank transferred from a given page to the targets of its outbound links upon the next iteration is divided equally among all outbound links. If the only links in the system were from pages B, C, and D to A, each link would transfer 0.25 PageRank to A upon the next iteration, for a total of 0.75. Since D had three outbound links, it would transfer one third of its existing value, or approximately 0.083, to A.

会发现将长故事的精华总结出来了。下面我们来创建一个函数 summary,输入文本并输出总结。不过还需设置两个条件:

  • 如果文本长度小于 500 个字符,则直接返回原始文本。总结会让文本的内容过于简短。

  • 如果文本只有一个句子,则 genism 无法处理,因为它只能选择文本中的重要句子。我们将使用 TextBlob 对象,该对象具有 .sentences 属性,可将文本分成多个句子。

import gensimfrom textblob import TextBlobdef summary(x):    if len(x) < 500 or str(TextBlob(x).sentences[0]) == x:        return x    else:        return gensim.summarization.summarize(x)data['Summary'] = data['Plot'].apply(summary)

如果不满足这两个条件中的任何一个,则进行常规的文本摘要。然后就是创建一列 summary。

数据集中一些示例文本的处理:

"The earliest known adaptation of the classic fairytale, this films shows Jack trading his cow for the beans, his mother forcing him to drop them in the front yard, and beig forced upstairs. As he sleeps, Jack is visited by a fairy who shows him glimpses of what will await him when he ascends the bean stalk. In this version, Jack is the son of a deposed king. When Jack wakes up, he finds the beanstalk has grown and he climbs to the top where he enters the giant's home. The giant finds Jack, who narrowly escapes. The giant chases Jack down the bean stalk, but Jack is able to cut it down before the giant can get to safety. He falls and is killed as Jack celebrates. The fairy then reveals that Jack may return home as a prince."

输出结果:

'As he sleeps, Jack is visited by a fairy who shows him glimpses of what will await him when he ascends the bean stalk.'

会发现生成的摘要满足预期,不仅易于阅读,而且对电影情节中的重要部分做了描述。

 

寻找最具代表性的情节

为了寻找最具有代表性的情节,需使用 K-Means 将情节文本分割成一定数量的簇。

按照文本的簇标签以及电影的国家、类型和年份将电影分成簇以方便查找。越是接近簇中心的电影,越能代表这个簇,因此最具有代表性。

这个想法背后的主要思想是:询问用户他们是否喜欢具有代表性的电影,为模型提供最多的信息,以补充关于用户喜好的资料。

从理论上说,最「准确」的推荐应当是在转换成非常长的图向量之后,推荐的图向量与原始文本的图向量之间存在某种相似性,但这个过程比较漫长。

在这里,我们利用摘要的属性来表示。

将文本划分成簇的工作只需进行一次,不仅可以为我们提供电影簇的其他功能,而且还可以为我们在实际提出推荐时提供电影的属性。

首先,需要删除所有标点符号,并将所有文本改为小写。我们使用正则表达式创建函数 clean() 来执行该操作。

import stringimport redef clean(text):    return re.sub('[%s]' % string.punctuation,'',text).lower()

然后使用 pandas 的 apply(),这个函数可应用于所有的图。

data['Cleaned'] = data['Plot'].apply(clean)


接下来,需要将数据变成向量,可采用 TF-IDF(term frequency–inverse document frequency)。

该方法可以区分词汇的重要性,方便将文本划分成簇。一个文档中出现多次,但在整个语料库中出现次数很少的单词,会被强化,而出现在所有文档中的单词会被弱化。

from sklearn.feature_extraction.text import TfidfVectorizervectorizer = TfidfVectorizer(stop_words='english',max_features=500)X = vectorizer.fit_transform(data['Plot'])

将这个非常稀疏的矩阵保存到变量 X 中,由于 K-Means 是基于距离的,意味着它会受到维数诅咒的影响,因此应尽最大努力来降低向量化文本的维数,这里将向量中的最大元素数设为 500。

出于同样的原因,在将数据输入到 K-Means 模型之前,最好先缩放数据。这里使用 StandardScaler 将数据缩放到 -1 到 1 之间。

from sklearn.preprocessing import StandardScalerscaler = StandardScaler()X = scaler.fit_transform(X)

下面就是训练 K-Means 模型。在理想情况下,簇的数量(我们需要提出的问题数量)应介于 3-6 之间(含 3 和 6)。

因此,我们使用列表 [3, 4, 5, 6] 中的每个簇来运行 K-Means 模型,评估每个簇的得分 ,并找出最适合所选数据的簇数量。

第一步,初始化存储簇的数量以及分数的两个列表(图中的 x 和 y ):

n_clusters = []scores = []

第二步,导入 sklearn 的 KMeans 和 silhouette_score。

from sklearn.cluster import KMeansfrom sklearn.metrics import silhouette_score

第三步,针对预先选择的四个簇数量中的每一个,拟合一个具有 n 个簇数量的 K-Means 模型,然后将该数量的簇的得分添加到列表中。

for n in [3,4,5,6]:    kmeans = KMeans(n_clusters=n)    kmeans.fit(X)    scores.append(silhouette_score(X,kmeans.predict(X)))    n_clusters.append(n)

第四步,只需点击 Kaggle 上的「提交」,然后让程序自己运行,经过几小时后就可完成。

最后的结果是:表现最佳的簇数量为三个,而且得分最高。

现在有了文本标签,可以开始将电影作为一个整体进行分簇了。但必须采取一些步骤来清理数据。

例如,Release Year 从 1900 年开始。如果采用文字整数值,模型会感到混乱。我们创建一个 Age 列来返回电影的年龄,简单地用 2017 年(数据库中最新的电影)减去电影发行的年份。

data['Age'] = data['Release Year'].apply(lambda x:2017-x)

Age 从 0 开始是有实际意义的。

Origin/Ethnicity 列很重要,因为故事的风格可以追溯到故事的来源。但是,该列有分类,例如可以是[ American,Telegu,Chinese ]。

如果想转换为机器可读的内容,需要对其进行 One-Hot 编码,可以通过 sklearn 的 OneHotEncoder 来实现。

from sklearn.preprocessing import OneHotEncoderenc = OneHotEncoder(handle_unknown=’ignore’)nation = enc.fit_transform(np.array(data[‘Origin/Ethnicity’]) .reshape(-1, 1)).toarray()

现在,nation 中保存了每一行的 One-Hot 编码编码值。行的每个索引代表一个唯一的值,例如,第一列(每行的第一个索引)代表「美国」。

但现在,它还只是一个数组,我们将需要创建数据中的列,将信息实际转换为数据。

因此需要将每一列命名为该向量的列对应的国家(enc.categories_ [0] 返回原始列的数组,nation[:,i] 索引指向数组中每一行的第 i 个值 )。

for i in range(len(nation[0])):    data[enc.categories_[0][i]] = nation[:,i]

至此,已成功地将每个故事的国籍添加到了数据中。接下来,我们对故事的类型做相同的处理。类型比国家更重要,因为它传达了关系到故事内容的信息,而这在机器学习模型识别的水平上是无法轻易实现的。

但是,有一个问题,看似很多类型分在了未知,这个问题稍后会进行解决。

data[‘Genre’].value_counts()

当前目标是对类型进行 One-Hot 编码。因为有太多类型由于其名称不同而被认为是不同的类型(例如「戏剧喜剧」和「浪漫喜剧」),但实际上都是同一种类型,我们选择了最流行的 20 种类型,其余的都归类到这 20 种类型中的一种。

top_genres = pd.DataFrame(data['Genre'].value_counts()).reset_index().head(21)['index'].tolist()top_genres.remove('unknown')

根据 top_genres 来处理类型,如果有的类型不在最流行的 20 种类型中,则将其替换为字符串「unknown」,它最终会被删除。

def process(genre):    if genre in top_genres:        return genre    else:        return 'unknown'data['Genre'] = data['Genre'].apply(process)

然后创建一个 One-Hot 编码器的实例,并将转换后的结果保存到变量 genres 中。

enc1 = OneHotEncoder(handle_unknown='ignore')genres = enc1.fit_transform(np.array(data['Genre']).reshape(-1, 1)).toarray()

为了将这个数组集成到数据中,需要再创建几列,每一列都用数组中的一列填充。

for i in range(len(genres[0])):    data[enc1.categories_[0][i]] = genres[:,i]

这里的数据是 One-Hot 编码,但仍然存在 unknown 值的问题,unknown 列的值为 1 的行需要设置类型。

因此,需要设置类型的每个索引,将其类型替换为 nan 值,以便稍后使用的 KNN 插值器时,可以识别出它是一个缺失值。

for i in data[data['unknown']==1].index:    for column in ['action',       'adventure', 'animation', 'comedy', 'comedy, drama', 'crime',       'crime drama', 'drama', 'film noir', 'horror', 'musical', 'mystery', 'romance', 'romantic comedy', 'sci-fi', 'science fiction', 'thriller', 'unknown', 'war', 'western']:        data.loc[i,column] = np.nan

通过上述操作,经缺失值标记成了缺失,现在可以使用 KNN 分类器了。

但除了上映的年份和国家以外,没有太多数据可用于分类。下面,我们使用 TF-IDF,从故事中选择前 30 个单词,作为 KNN 正确分配类型的附加信息。

在操作时必须事先清理文本,因此使用正则表达式来删除所有标点符号,并将所有本文都转换为小写。

import redata['Cleaned'] = data['Plot'].apply(lambda x:re.sub('[^A-Za-z0-9]+',' ',str(x)).lower())

我们将设置英语标准的停用词,并将特征的最大数量设置为 30。经过清理后向量化的文本以数组的形式存储到变量 X。

from sklearn.feature_extraction.text import TfidfVectorizervectorizer = TfidfVectorizer(stop_words=’english’,max_features=30)X = vectorizer.fit_transform(data[‘Cleaned’]).toarray()

然后将数组 X 中的每一列信息都转移成我们数据的一列,并命名每一列为 x 中相应列的单词。

keys = list(vectorizer.vocabulary_.keys())for i in range(len(keys)):    data[keys[i]] = X[:,i]

这些单词将提供更多背景信息,帮助设置类型。最后,可以来设置类型了。

from sklearn.impute import KNNImputerimputer = KNNImputer(n_neighbors=5)column_list = ['Age', 'American', 'Assamese','Australian', 'Bangladeshi', 'Bengali', 'Bollywood', 'British','Canadian', 'Chinese', 'Egyptian', 'Filipino', 'Hong Kong', 'Japanese','Kannada', 'Malayalam', 'Malaysian', 'Maldivian', 'Marathi', 'Punjabi','Russian', 'South_Korean', 'Tamil', 'Telugu', 'Turkish','man', 'night', 'gets', 'film', 'house', 'takes', 'mother', 'son','finds', 'home', 'killed', 'tries', 'later', 'daughter', 'family','life', 'wife', 'new', 'away', 'time', 'police', 'father', 'friend','day', 'help', 'goes', 'love', 'tells', 'death', 'money', 'action', 'adventure', 'animation', 'comedy', 'comedy, drama', 'crime','crime drama', 'drama', 'film noir', 'horror', 'musical', 'mystery','romance', 'romantic comedy', 'sci-fi', 'science fiction', 'thriller','war', 'western']imputed = imputer.fit_transform(data[column_list])

设置类型的时候能够识别出缺失值 np.nan,并自动使用周围的国家数据和数据中的单词以及电影的年龄来估计类型。结果保存到数组形式的变量中。

和前面的操作一样,我们将数据转换为:

for i in range(len(column_list)):    data[column_list[i]] = imputed[:,i]

之后,需删除 One-Hot 编码或不再需要的列,例如 Genre 的 Unknown 或类别 Genre 变量……

data.drop(['Title','Release Year','Director','Cast','Wiki Page','Origin/Ethnicity','Unknown','Genre'],axis=1,inplace=True)

……数据已准备就绪,没有缺失值。KNN 分类的一个特点是,它可以给出十进制的值,如一部电影 20% 是西方,其余部分是另一种或几种类型。

这些特征与之前获得的簇标签相结合,可以很好地表明用户对某个故事的喜爱程度。最后需要和开始分簇,像以前一样,我们将故事分为 3、4、5 或 6 个簇,然后看看哪种表现最佳。

from sklearn.cluster import KMeansfrom sklearn.metrics import silhouette_scoreXcluster = data.drop(['Plot','Summary','Cleaned'],axis=1)score = []for i in [3,4,5,6]:    kmeans = KMeans(n_clusters=i)    prediction = kmeans.fit_predict(Xcluster)    score = silhouette_score(Xcluster,prediction)    score.append(score)

绘制得分情况……

和前面一样,三个簇的时候表现最好,得分最高。所以我们仅在三个簇上训练 K-Means:

from sklearn.cluster import KMeansXcluster = data.drop(['Plot','Summary','Cleaned'],axis=1)kmeans = KMeans(n_clusters=3)kmeans.fit(Xcluster)pd.Series(kmeans.predict(Xcluster)).value_counts()

最好让每个簇都拥有数量差不多的电影,可通过 .cluster_centers_ 方法来获得簇的中心:

centers = kmeans.cluster_centers_centers

首先,我们为每一项分配标签。

Xcluster['Label'] = kmeans.labels_

对于每个簇,我们希望找到距离簇中心欧几里得距离最近的数据点,该点最能代表整个簇。

可以参考欧几里得距离公式:p 和 q 两点之间的距离由 p 和 q 对应维度之差的平方和,再取平方根得到。

由于欧几里得距离是 L2 范数,因此可以使用 numpy 的线性代数函数 np.linalg.norm(a-b) 来计算。

下面我们来看看完整的计算代码,并找到与簇之间的欧几里得距离最小的故事。

for cluster in [0,1,2]:    subset = Xcluster[Xcluster['Label']==cluster]    subset.drop(['Label'],axis=1,inplace=True)    indexes = subset.index    subset = subset.reset_index().drop('index',axis=1)    center = centers[cluster]    scores = {'Index':[],'Distance':[]}

上述代码可以初始化搜索。

  • 首先,将标签与我们当前正在搜索的簇相符的故事保存起来。

  • 然后,我们从子集中删除 Label。为了保存原始的索引以供以后参考,我们将索引存储到变量 indexes 中。

  • 接下来,我们将重置子集上的索引,以确保索引正常工作。

  • 最后,我们选择当前簇的中心点,并初始化一个包含两列的字典:一个保存主数据集中的故事索引的列表,另一个存储得分/距离的列表。

for index in range(len(subset)):   scores['Index'].append(indexes[index])   scores['Distance'].append(np.linalg.norm(center-np.array( subset.loc[index])))

这段代码会遍历子集中的每一行,记录当前索引,并计算和记录它与中心之间的距离。

scores = pd.DataFrame(scores)    print('Cluster',cluster,':',scores[scores['Distance']==scores['Distance'].min()]['Index'].tolist())

上述代码将分数转换为 pandas DataFrame 以进行分析,并输出距中心最近的故事的索引。

似乎第一个簇中具有最小欧几里德距离的故事有四个,而簇 1 和 2 只含有一个故事。

簇 0:

data.loc[4114]['Summary']

输出:

'On a neutral island in the Pacific called Shadow Island (above the island of Formosa), run by American gangster Lucky Kamber, both sides in World War II attempt to control the secret of element 722, which can be used to create synthetic aviation fuel.'

簇 1:

data.loc[15176]['Summary']

输出:

'Jake Rodgers (Cedric the Entertainer) wakes up near a dead body. Freaked out, he is picked up by Diane.'

簇 2:

data.loc[9761]['Summary']

输出:

'Jewel thief Jack Rhodes, a.k.a. "Jack of Diamonds", is masterminding a heist of $30 million worth of uncut gems. He also has his eye on lovely Gillian Bromley, who becomes a part of the gang he is forming to pull off the daring robbery. However, Chief Inspector Cyril Willis from Scotland Yard is blackmailing Gillian, threatening her with prosecution on another theft if she doesn\'t cooperate in helping him bag the elusive Rhodes, the last jewel in his crown before the Chief Inspector formally retires from duty.'

现在我们获得了三个最有代表性的故事情节。虽然人类看不出其中的区别,但在机器学习模型的心中,这些数据为它提供了大量信息,可供随时使用。

 

推荐引擎:让系统更懂你

推荐引擎可以预测哪些电影情节更有可能获得用户的高度评价。引擎接收电影的特征,例如年龄或国家,以及 TF-IDF 向量化的摘要,最大可接收 100 个特征。

每个电影情节的目标是 1 或 0。模型经过在数据(用户已评价的故事)上的训练后,可预测用户对故事评价良好的概率。

反过来,模型会向用户推荐最有可能受到喜爱的故事,并记录用户对该故事的评分,最后还会将该故事添加到训练数据列表中。

至于训练数据,我们仅使用每部电影中数据的属性:

这里可能需要决策树分类器,因为它可以做出有效的预测,快速训练并开发高方差解决方案,这正是推荐系统所追求的目的。

 

综合所有组件,输出你的故事

一切都梳理完毕后,就可以将所有的组件集合到一起了。

首先,针对三个最有代表性的电影,编写用户的评分。这个程序会确保针对每个输入,输出为 0 或 1。

import timestarting = []print("Indicate if like (1) or dislike (0) the following three story snapshots.")print("\n> > > 1 < < <")print('On a neutral island in the Pacific called Shadow Island (above the island of Formosa), run by American gangster Lucky Kamber, both sides in World War II attempt to control the secret of element 722, which can be used to create synthetic aviation fuel.')time.sleep(0.5) #Kaggle sometimes has a glitch with inputswhile True:    response = input(':: ')    try:        if int(response) == 0 or int(response) == 1:            starting.append(int(response))            break        else:            print('Invalid input. Try again')    except:        print('Invalid input. Try again')print('\n> > > 2 < < <')print('Jake Rodgers (Cedric the Entertainer) wakes up near a dead body. Freaked out, he is picked up by Diane.')time.sleep(0.5) #Kaggle sometimes has a glitch with inputswhile True:    response = input(':: ')    try:        if int(response) == 0 or int(response) == 1:            starting.append(int(response))            break        else:            print('Invalid input. Try again')    except:        print('Invalid input. Try again')print('\n> > > 3 < < <')print("Jewel thief Jack Rhodes, a.k.a. 'Jack of Diamonds', is masterminding a heist of $30 million worth of uncut gems. He also has his eye on lovely Gillian Bromley, who becomes a part of the gang he is forming to pull off the daring robbery. However, Chief Inspector Cyril Willis from Scotland Yard is blackmailing Gillian, threatening her with prosecution on another theft if she doesn't cooperate in helping him bag the elusive Rhodes, the last jewel in his crown before the Chief Inspector formally retires from duty.")time.sleep(0.5) #Kaggle sometimes has a glitch with inputswhile True:    response = input(':: ')    try:        if int(response) == 0 or int(response) == 1:            starting.append(int(response))            break        else:            print('Invalid input. Try again')    except:        print('Invalid input. Try again')

上述代码运行无误后,需要将数据存储到训练数据集 DataFrame 中,然后删除数据中的索引。

X = data.loc[[9761,15176,4114]].drop( ['Plot','Summary','Cleaned'],axis=1)y = startingdata.drop([[9761,15176,4114]],inplace=True)

然后创建一个循环,并在当前训练集上训练决策树分类器。

from sklearn.tree import DecisionTreeClassifiersubset = data.drop(['Plot','Summary','Cleaned'],axis=1)while True:    dec = DecisionTreeClassifier().fit(X,y)

接下来,要针对数据中的每个索引,进行概率预测。

dic = {'Index':[],'Probability':[]}subdf = shuffle(subset).head(10_000) #select about 1/3 of datafor index in tqdm(subdf.index.values):     dic['Index'].append(index)     dic['Probability'].append(dec.predict_proba(  np.array(subdf.loc[index]).reshape(1, -1))[0][1])     dic = pd.DataFrame(dic)

为了确保快速选择,我们在打乱的数据中随机选择大约 1/3 的数据,并选择前 10,000 行。这段代码将索引保存到 DataFrame。

最初,许多电影的概率都为 1,但随着我们的进步和模型的学习,它将开始做出更高级的选择。

index = dic[dic['Probability']==dic['Probability'].max()] .loc[0,'Index']

系统会将用户最喜爱的电影的索引保存到变量 index,我们需要从数据中获取有关索引的信息并显示它。

print('> > > Would you be interested in this snippet from a story? (1/0/-1 to quit) < < <')print(data.loc[index]['Summary'])time.sleep(0.5)

然后验证用户的输入是 0、1 还是 -1(退出):

while True:        response = input(':: ')        try:            if int(response) == 0 or int(response) == 1:                response = int(response)                break            else:                print('Invalid input. Try again')        except:            print('Invalid input. Try again')

我们还可以开始添加训练数据。但是必须考虑允许用户在需要退出的时候结束循环。

if response == -1:        break

另外,无论用户喜欢还是不喜欢这部电影,我们都将其添加到训练数据中(目标将有所不同):

X = pd.concat([X,pd.DataFrame(data.loc[index].drop(['Plot','Summary','Cleaned'])).T])

最后,如果响应为 0,就将 0 添加到 y 中,表示用户不想听这个故事。

if response == 0:        y.append(0)

如果用户喜欢这个故事,则程序输出完整的故事。

else:        print('\n> > > Printing full story. < < <')        print(data.loc[index]['Plot'])        time.sleep(2)        print("\n> > > Did you enjoy this story? (1/0) < < <")

再次收集用户的输入,并确保输入为 0 或 1。

while True:      response = input(':: ')      try:          if int(response) == 0 or int(response) == 1:              response = int(response)              break          else:              print('Invalid input. Try again')      except:          print('Invalid input. Try again')

并相应地将 0 或 1 添加到 y。

if response == 1:      y.append(1)else:      y.append(0)

当然要记得从数据中删除操作过的故事,因为没有人想重复看到同一个故事。

data.drop(index,inplace=True)

至此所有的步骤都进行完了,每次迭代都会更新训练数据,模型的准确率也会越来与高。

所有人都可以通过这个程序来阅读一些有趣的故事,或查看这些情节出自哪部电影。在隔离期间,处理数据方面的问题和难题非常有意思,可以为我们带来一丝乐趣。

希望你能够喜欢本文的实战操作,如果你想试试看这个程序,那么请点击这里获取:

https://www.kaggle.com/washingtongold/tell-me-a-story-1-2?scriptVersionId=31773396

原文连接:

https://towardsdatascience.com/tell-me-a-story-ai-one-that-i-like-4c0bc60f46ae

—— 完 ——

扫描二维码,加入讨论群

获得更多优质数据集

了解人工智能落地应用

关注顶会&论文

回复「读者」了解更多

更多精彩内容(点击图片阅读)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值