這段日子裏,我們都被隔離了,就特別想聽故事。然而,我們並非對所有故事都感興趣,有些人喜歡浪漫的故事,他們肯定不喜歡懸疑小説,而喜歡推理小説的人肯定對浪漫的愛情故事沒興趣。看看周圍,還有誰比AI更擅長講我們喜歡的故事呢?
在本文中,我將向你演示如何編寫一個AI,根據我們的個人喜好來給我們講故事,為沉悶的隔離生活增添一份樂趣。
本文可以分為以下幾個部分:
2.程序演示:在完成編寫代碼的工作後,作為預覽演示系統的功能。
3.數據加載和清理:加載數據並準備好進行處理。
4.尋找最具有代表性的情節:該項目的第一部分,使用K-Means選擇用户最感興趣的情節。
6.推薦引擎:使用簡單的預測式機器學習模型推薦新故事。
7.綜合所有組件:編寫能夠將所有組件結合在一起的生態系統結構。
藍圖
我想讓AI給我講個故事。在理想情況下,我希望以真正的技術-文藝復興時期的方式來訓練遞歸神經網絡或其他的生成式方法。然而,以我從事文本生成工作的經驗來看,這些訓練要麼需要花費很長很長的時間,要麼就會出現過度擬合數據,導致無法完成“原始文本生成”的目標。另外,還需注意,訓練一個性能良好的模型所需的時間超過8個小時,然而據我所知,訓練深度學習模型最有效的免費平台Kaggle最多隻能免費運行8小時。
我想創建一個快速、通用且每個人都可以實現的項目。這個AI無需訓練RNN或生成模型,只需從“故事數據庫”中搜索人為創建的故事,然後找到我最喜歡的故事。這不僅可以保證故事的基本質量,而且速度更快。
至於“故事數據庫”,我們來使用Kaggle上的Wikipedia電影情節數據集。其中包含了各種類型、國家和時代的3.5萬個電影故事,可謂是眼前我所能找到的最佳故事數據庫。
該數據集包括髮行年份、標題、電影的國家、類型和劇情的文字説明。
現在數據已就緒,接下來我們來設計一個粗略的大綱/藍圖。
1.這個程序會輸出五個特性鮮明的故事的概要
2.用户的評分,他們是喜歡、不喜歡還是保持中立。
3.這個程序接收用户對這五個故事的喜好程度,並輸出完整故事的摘要。如果用户感興趣,則程序會輸出完整的故事。每個完整的故事結束後,程序都會要求用户提供反饋。該程序將從實時反饋中學習,並嘗試提出更好的推薦。
注意,我們選擇了五個左右最有代表性的故事,目的是為了讓模型在有限的數據量下獲得儘可能多的信息。
系統演示
剛開始的時候,這個程序會要求你針對三個故事提供反饋。對於程序來説,這三個故事是數據的每個簇中最具代表性的故事。
在回答完前三個入門問題,對你的喜好進行大致評估後,模型就會開始生成你喜歡的故事。
如果你對某個故事的節選感興趣,那麼程序就會輸出整個故事供你閲讀。
模型會將你的反饋添加到訓練數據,以改善模型的推薦。當你閲讀故事時,模型會不斷學習。如果你不喜歡某個故事的摘要,那麼程序就不會輸出完整的故事,它會繼續生成新的故事。
如果你喜歡某個謀殺和警察的故事節選,並給出了“1”作為響應,那麼程序就會開始學習,並朝着這個方向推薦越來越多的故事。
這個程序就像“蒙特卡洛樹搜索”一樣,朝着優化獎勵的方向發展,並在偏離太遠時後退,從而優化你的體驗。
數據加載和清理
我們通過pandas 的 load_csv加載數據。
importpandas aspd data= pd.read_csv data.head
數據集中的字段包括髮行年份、電影名稱、國家、導演、演員、類型、該電影在Wikipedia頁面上的URL以及劇情的文字説明。我們可以去掉導演和演員陣容,對於我們的推薦算法或聚類方法來説,這兩個字段的分類太多了,所以收益不大。然而,電影類型的數量相對較少——100多部電影的分類只有30多個,而且這代表了超過80%的電影。因此,我們可以刪除導演和演員。
data.drop
我們遇到的另一個問題是括號的引用。眾所周知,Wikipedia會針對引用來源編號。
"Grace Roberts , marries rancher Edward Smith, who is revealed to be a neglectful, vice-ridden spouse. They have a daughter, Vivian. Dr. Franklin whisks Grace away from this unhappy life, and they move to New York under aliases, pretending to be married . Grace and Franklin have a son, Walter . 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 withbothchildren. "
例如,對於上述字符串,我們需要刪除。最簡單的解決方案是創建一個帶有每個括號值的列表,然後從字符串中刪除列表中存在的每個值。這種方法的前提是我們可以確保每篇文章的引用都不會超過99條。儘管效率不是最高,但我們可以通過混亂的字符串索引或拆分來解決這個問題。
blacklist = []fori in range: blacklist. append
這段代碼創建了blacklist,這個列表包含了我們不想要的引用標記。
def remove_brackets: foritem inblacklist: string= string.replace returnstring
接下來,我們可以使用這個blacklist創建一個函數remove_brackets,然後應用到每一列。
data = data.apply
至此,我們的基本數據清理工作結束了。
總結故事情節
這個系統的關鍵要素是總結故事情節。由於通常故事讀起來都太長,因此總結故事很重要,方便用户選擇是否繼續閲讀。
我們將使用基於圖的摘要算法,這是最流行的文本摘要方法。首先創建文檔單元圖,然後選擇具有適用於此場景的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 valuestomore closely reflect the theoretical truevalue. Assume a small universe offour web pages: A, B, C andD. Links froma page toitself, ormultiple outbound links fromone single page toanother single page, areignored. PageRank isinitializedtothe same valueforallpages. Inthe original formofPageRank, the sumofPageRank overallpages was the total numberofpages onthe web atthat time, so eachpage inthis example would have an initialvalueof1.However, later versionsofPageRank, andthe remainderofthis section, assume a probability distribution between0and1.Hence the initialvalueforeachpage inthis example is0.25. The PageRank transferred froma given page tothe targets ofits outbound links upon the nextiteration isdivided equally among alloutbound links. Ifthe onlylinks inthe systemwere frompages B, C, andD toA, eachlinkwould transfer 0.25PageRank toA upon the nextiteration, fora total of0.75. Suppose instead that page B had a linktopages C andA, page C had a linktopage A, andpage D had links toallthree pages. Thus, upon the firstiteration, page B would transfer half ofits existing value, or0.125, topage A andthe other half, or0.125, topage C. Page C would transfer allofits existing value, 0.25, tothe onlypage it links to, A. Since D had three outbound links, it would transfer one third ofits existing value, orapproximately 0.083, toA. Atthe completion ofthis iteration, page A will have a PageRank ofapproximately 0.458. Inother words, the PageRank conferred byan outbound linkisequal tothe document’s own PageRank score divided bythe numberofoutbound links L. Inthe generalcase, the PageRank valueforanypage u can be expressed as: i.e. the PageRank valuefora page u isdependent onthe PageRank valuesforeachpage v contained inthe setBu , divided bythe numberL oflinks frompage v. The algorithm involves a damping factor forthe calculation ofthe pagerank. It islikethe income tax which the govt extracts fromone despite paying him itself. '''print)
輸出:
In the original form ofPageRank, the sum ofPageRank over all pages was the total number ofpages on the web at that time, so each page inthisexample would have an initial value of1.The PageRank transferred froma given page to the targets ofits outbound links upon the next iteration is divided equally among all outbound links. If the only links inthe system were frompages B, C, and D to A, each link would transfer 0.25PageRank to A upon the next iteration, fora total of0.75. Since D had three outbound links, it would transfer one third ofits existing value, or approximately 0.083, to A.
這段總結得很不錯。圖摘要算法是最有效的總結方法之一,我們將使用該算法總結摘要。下面我們來創建一個函數summary,接收文本並輸出總結。但是,我們需要設置兩個條件:
如果文本長度小於500個字符,則直接返回原始文本。總結會讓文本的內容過於簡短。
如果文本只有一個句子,則genism 無法處理,因為它只能選擇文本中的重要句子。我們將使用TextBlob對象,該對象具有.sentences屬性,可將文本分成多個句子。如果文本的第一個句子就等於文本本身,則可以判斷該文本只有一個句子。
importgensim fromtextblob importTextBlob defsummary: iflen <>
如果不滿足這兩個條件中的任何一個,則返回文本的摘要。接下來,我們創建一列summary。
運行需要花費幾個小時。但是,只需運行一次,而且總結完成後還可以節省以後的時間。
讓我們來看看數據集中一些示例文本的處理:
"The earliest known adaptation of the classic fairytale, this films shows Jack trading his cow for the beans, his mother forcing him to dropthem inthe front yard, andbeig forced upstairs. Ashe sleeps, Jack isvisited bya fairy who shows him glimpses ofwhat will await him whenhe ascends the bean stalk. Inthis version, Jack isthe son ofa deposed king. WhenJack wakes up, he finds the beanstalk has grown andhe climbs tothe top wherehe 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來執行該操作。
importstring importre defclean: returnre.sub.lower
我們使用 pandas 的 apply,這個函數可應用於所有的圖。
data = data.apply
接下來,我們將數據變成向量。我們使用TF-IDF。該方法可以幫助我們區分重要的詞和不重要的詞,方便將文本劃分成簇。該方法可以強調在一個文檔中出現多次,但在整個語料庫中出現次數很少的單詞,並弱化那些出現在所有文檔中的單詞。
fromsklearn.feature_extraction.text importTfidfVectorizer vectorizer = TfidfVectorizer X = vectorizer.fit_transform
我們將這個非常稀疏的矩陣保存到變量X中。由於K-Means是基於距離的,這意味着它會受到維數詛咒的影響,因此我們應盡最大努力來降低向量化文本的維數,這裏我們將向量中的最大元素數為500。應介於3-6之間。
因此,我們使用列表中的每個簇來運行K-Means模型。我們將評估每個簇的得分 ,並找出最適合我們數據的簇數量。
首先,我們來初始化存儲簇的數量以及分數的兩個列表:
n_clusters= [] scores= []
接下來,我們導入sklearn 的 KMeans 和 silhouette_score。
fromsklearn.cluster importKMeans fromsklearn.metrics importsilhouette_score
然後,我們針對預先選擇的四個簇數量中的每一個,擬合一個具有n個簇數量的KMeans模型,然後將該數量的簇的得分添加到列表中。
forn in : kmeans = KMeanskmeans.fitscores. append)) n_clusters. append
接下來,我只需點擊Kaggle上的“提交”,然後讓程序自己運行,這需要幾個小時才能完成。
最後的結果是:表現最佳的簇數量為三個,而且得分最高。
現在我們有了文本標籤,可以開始將電影作為一個整體進行分簇了。但是,我們必須採取一些步驟來清理數據。
例如,Release Year從1900年開始。如果採用文字整數值,那麼模型就會很迷惑。我們創建一個Age列來返回電影的年齡,簡單地用2017年減去電影發行的年份。
data = data.apply
Age從0開始是有實際意義的。
Origin/Ethnicity列很重要,故事的風格可以追溯到故事的來源。但是,該列有分類,例如可以是。如果想轉換為機器可讀的內容,我們需要對其進行One-Hot編碼,我們通過 sklearn 的 OneHotEncoder 來實現。
from sklearn.preprocessing import OneHotEncoderenc = OneHotEncodernation = enc.fit_transform .reshape).toarray
現在,nation中保存了每一行的One-Hot編碼編碼值。行的每個索引代表一個唯一的值,例如,第一列代表“美國”。
但是,目前,它只是一個數組,我們將需要創建數據中的列,將信息實際轉換為我們的數據。因此,我們將每一列命名為該向量的列對應的國家。
fori inrange): data]= nation
我們已成功地將每個故事的國家添加到了我們的數據中了。接下來,我們對故事的類型做相同的處理。類型比國家更重要,因為它傳達了關係到故事內容的信息,而這在機器學習模型識別的水平上是無法輕易實現的。
但是,有一個問題:
data.value_counts
似乎很多類型都是未知的。不過不用擔心,我們稍後再解決。目前,我們的目標是對類型進行One-Hot編碼。我們按照上述方式,但會稍作改動,因為有太多類型由於其名稱不同而被認為是不同的類型,但實際上都是同一種類型,我們只選擇最流行的20種類型,其餘的都歸類到這20種類型中的一種。
top_genres = pd.DataFrame.reset_index.head.tolist top_genres. remove
請注意,最終我們會刪除列表中的“unknown”,這就是為什麼最初出現了21個類型的原因。接下來,讓我們根據top_genres來處理類型,如果有的類型不在最流行的20種類型中,則將其替換為字符串“unknown”。
def process:ifgenre intop_genres: returngenre else: return'unknown'data = data.apply
然後,像上面一樣,我們創建一個One-Hot編碼器的實例,並將轉換後的結果保存到變量genres中。
enc1= OneHotEncoder genres= enc1.fit_transform.reshape).toarray
為了將這個數組集成到數據中,我們再來創建幾列,每一列都用數組中的一列填充。
fori inrange): data]= genres
我們的數據是One-Hot編碼,但仍然存在unknown值的問題。現在,所有數據均已完成One-Hot編碼,我們知道,unknown列的值為1的行需要設置類型。因此,我們針對需要設置類型的每個索引,將其類型替換為nan值,以便我們稍後使用的KNN插值器時,可以識別出它是一個缺失值。
fori indata== 1].index: forcolumn in: data.loc = np.nan
現在,所有缺失值都標記成了缺失,我們可以使用KNN分類器了。但是,除了上映的年份和國家以外,我們沒有太多數據可用於分類。下面,我們使用TF-IDF,從故事中選擇前30個單詞,作為KNN正確分配類型的附加信息。
我們必須事先清理文本,因此我們使用正則表達式來刪除所有標點符號,並將所有本文都轉換為小寫。
importre data = data.apply).lower)
我們將設置英語標準的停用詞,並將特徵的最大數量設置為30。經過清理後向量化的文本以數組的形式存儲到變量X。
from sklearn.feature_extraction.text import TfidfVectorizervectorizer = TfidfVectorizerX = vectorizer.fit_transform.toarray
像上面一樣,我們將數組X中的每一列信息都轉移成我們數據的一列,並命名每一列為x中相應列的單詞。
keys= list fori in range): data] = X
這些單詞將提供更多背景信息,幫助設置類型。最後,我們來設置類型!
fromsklearn.impute importKNNImputer imputer = KNNImputer column_list = imputed = imputer.fit_transform
設置類型的時候能夠識別出缺失值np.nan,並自動使用周圍的國家數據和數據中的單詞以及電影的年齡來估計類型。結果保存到數組形式的變量中。與往常一樣,我們將數據轉換為:
fori inrange): data]= imputed
刪除One-Hot編碼或不再需要的列之後,例如 Genre 的 Unknown 或 類別 Genre 變量……
data.drop
……數據已準備就緒,沒有缺失值。KNN分類的另一個有趣的方面是,它可以給出十進制的值,也就是説,一部電影20%是西方,其餘部分是另一種或幾種類型。
這些特徵都可以很好地用於簇。這些特徵與之前獲得的簇標籤相結合,應該可以很好地表明用户對某個故事的喜愛程度。最後,我們開始分簇,像以前一樣,我們將故事分為3、4、5或6個簇,然後看看哪種表現最佳。
fromsklearn.cluster importKMeans fromsklearn.metrics importsilhouette_score Xcluster = data.drop score = []fori in: kmeans = KMeansprediction = kmeans.fit_predictscore = silhouette_scorescore.append
繪製得分情況……
像前面一樣,三個簇的表現最好,得分最高。所以我們僅在三個簇上訓練KMeans:
fromsklearn.cluster importKMeans Xcluster = data.drop kmeans = KMeans kmeans.fitpd.Series).value_counts
最好讓每個簇都擁有數量差不多的電影。我們可以通過.cluster_centers_方法來獲得簇的中心:
centers = kmeans.cluster_centers_centers
首先,我們為每一項分配標籤。
Xcluster = kmeans.labels_
對於每個簇,我們希望找到距離簇中心歐幾里得距離最近的數據點。該點最能代表整個簇。p和q兩點之間的距離由p和q對應維度之差的平方和,再取平方根。你可以參考歐幾里得距離公式:
由於歐幾里得距離是l2範數,因此可以使用numpy的線性代數函數np.linalg.norm來計算。
下面我們來看看完整的計算代碼,並找到與簇之間的歐幾里得距離最小的故事。
forcluster in: subset = Xcluster==cluster] subset.drop indexes = subset.indexsubset = subset.reset_index.drop center = centersscores = { 'Index':[], 'Distance':[]}
上述代碼可以初始化搜索。首先,將標籤與我們當前正在搜索的簇相符的故事保存起來。然後,我們從子集中刪除Label。為了保存原始的索引以供以後參考,我們將索引存儲到變量indexes中。接下來,我們將重置子集上的索引,以確保索引正常工作。然後,我們選擇當前簇的中心點,並初始化一個包含兩列的字典:一個保存主數據集中的故事索引的列表,
另一個存儲得分/距離的列表。
forindexinrange): scores.append scores.append))
這段代碼會遍歷子集中的每一行,記錄當前索引,並計算和記錄它與中心之間的距離。
scores = pd.DataFrameprint
這段代碼將分數轉換為pandas DataFrame以進行分析,並輸出距中心最近的故事的索引。
似乎第一個簇中具有最小歐幾里德距離的故事有四個,而簇1和2只有一個故事。
簇0:
data.loc
輸出:
' Ona neutral island inthe Pacificcalled ShadowIsland, run by Americangangster LuckyKamber, both sides inWorldWarIIattempt to control the secret of element 722, which can be used to create synthetic aviation fuel.'
簇1:
data.loc
輸出:
'Jake Rodgers wakes up near a dead body. Freaked out, he is picked up by Diane.'
簇2:
data.loc
輸出:
' Jewelthief JackRhodes, a.k.a. "Jack of Diamonds", ismasterminding a heist of $ 30million worth of uncut gems. Healso has his eye on lovely GillianBromley, who becomes a part of the gang he isforming to pull off the daring robbery. However, ChiefInspectorCyrilWillisfrom ScotlandYardisblackmailing Gillian, threatening her with prosecution on another theft ifshe doesn't cooperate inhelping him bag the elusive Rhodes, the last jewel inhis crown before the ChiefInspectorformally retires from duty.'
很好!現在我們獲得了三個最有代表性的故事情節。雖然人類看不出其中的區別,但在機器學習模型的心中,這些數據為它提供了大量信息,可供隨時使用。
推薦引擎
這裏的推薦引擎只是一個機器學習模型,可以預測哪些電影情節更有可能獲得用户的高度評價。該引擎接收電影的特徵,例如年齡或國家,以及TF-IDF向量化的摘要,最大可接收100個特徵。
每個電影情節的目標是1或0。模型經過在數據上的訓練後,可預測用户對故事評價良好的概率。接下來,模型會向用户推薦最有可能受到喜愛的故事,並記錄用户對該故事的評分,最後還會將該故事添加到訓練數據列表中。
至於訓練數據,我們僅使用每部電影中數據的屬性。
我們可能需要決策樹分類器,因為它可以做出有效的預測,快速訓練並開發高方差解決方案,這正是推薦系統所追求的。
綜合所有組件
首先,我們針對三個最有代表性的電影,編寫用户的評分。這個程序會確保針對每個輸入,輸出為0或1。
import timestarting = []print or dislike the following three story snapshots.") print print, 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 Kaggle sometimes has a glitch with inputswhileTrue: response = input try:ifint == 0orint == 1: starting.append) breakelse: print except:print print print time.sleep select about 1/3 of dataforindexin tqdm: dic.append dic.append.reshape)) dic = pd.DataFrame
為了確保快速選擇,我們在打亂的數據中隨機選擇大約1/3的數據,並選擇前10,000行。這段代碼將索引保存到DataFrame。
最初,許多電影的概率都為1,但隨着我們的進步和模型的學習,它將開始做出更高級的選擇。
index= dic==dic.max] .loc
我們將用户最喜愛的電影的索引保存到變量index。
下面,我們需要從數據中獲取有關索引的信息並顯示它。
print <>
然後驗證用户的輸入是0、1還是-1:
whileTrue: response = input try:ifint == 0orint == 1: response = int breakelse: print except:print
……我們可以開始添加訓練數據。但是,首先,我們必須允許用户在需要退出的時候結束循環。
ifresponse == - 1: break
另外,無論用户喜歡還是不喜歡這部電影,我們都將其添加到訓練數據中:
X= pd.concat).T])
最後,如果響應為0,我們將0添加到y中。表示用户不想聽這個故事。
ifresponse == 0: y. append
如果用户喜歡這個故事,則程序輸出完整的故事。
else: print print time.sleep print <>
我們再次收集用户的輸入,並確保輸入為0或1。
whileTrue: response = input try:ifint == 0orint == 1: response = int breakelse: print except:print
……並相應地將0或1添加到y。
ifresponse == 1: y. append else: y. append
最後,我們從數據中刪除故事,因為用户不想重複看到同一個故事。
data.drop
大功告成!每次迭代都會更新訓練數據,模型的準確率也會越來與高。
感謝您的閲讀!
希望您喜歡本文!你可以通過這個程序來閲讀一些有趣的故事,或查看這些情節出自哪部電影。在隔離期間,處理數據方面的問題和難題非常有意思,可以為我們帶來一絲樂趣。
如果你想試試看這個程序,那麼請點擊這裏獲取:
https://www.kaggle.com/washingtongold/tell-me-a-story-1-2?VersionId=31773396
原文連接:
https://towardsdatascience.com/tell-me-a-story-ai-one-that-i-like-4c0bc60f46ae