離線文章畫像計算

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度學習實戰(不定時更新)


2.4 離線文章畫像計算

學習目標

  • 目標
    • 瞭解文章畫像構成
    • 知道spark tfidf以及TextRank計算工具使用
    • 知道文章畫像的計算和構建
  • 應用
    • 應用spark完成文章Tfidf值計算
    • 應用spark完成文章TextRank值計算
    • 應用spark完成文章畫像結果值計算與存儲

離線文章畫像組成需求

文章畫像,就是給每篇文章定義一些詞。主題詞與關鍵詞最大的區別就是主題詞經過了規範化處理。

  • 關鍵詞:關鍵詞則是作者自由定義的,通常是文章主題內容的高度概括,比主題詞更明確、更具體。

    • 通過分詞+TFIDF/TextRank獲取文章中一些詞的權重高的。
  • 主題詞:主題詞是由機構定義和發佈的規範詞,通常是專有名詞或名詞短語;

    • 進行規範化處理的,文章中出現的不同方法結果中同義詞或者同時出現的詞
  • 關鍵詞:TFIDF/TextRank計算出的結果TOPK個詞以及權重

  • 主題詞:TextRank的TOPK詞 與 ITFDF計算的TOPK個詞的交集

離線文章畫像的存儲

文章畫像結果存儲在HIVE當中,後續作爲離線特徵分析的數據源。HIVE中有一個article數據庫,存儲文章相關中間計算結果以及畫像結果;

hive> show tables;
OK
article_data
article_profile
article_vector
idf_keywords_values
textrank_keywords_values
tfidf_keywords_values
Time taken: 0.174 seconds, Fetched: 6 row(s)

查看結果:

hive> desc article_profile;
OK
article_id              int                     article_id          
channel_id              int                     channel_id          
keywords                map      keywords            
topics                  array           topics      

hive> select * from article_profile limit 1;
OK
26      17      {"策略":0.3973770571351729,"jpg":0.9806348975390871,"用戶":1.2794959063944176,"strong":1.6488457985625076,"文件":0.28144603583387057,"邏輯":0.45256526469610714,"形式":0.4123994242601279,"全自":0.9594604850547191,"h2":0.6244481634710125,"版本":0.44280276959510817,"Adobe":0.8553618185108718,"安裝":0.8305037437573172,"檢查更新":1.8088946300014435,"產品":0.774842382276899,"下載頁":1.4256311032544344,"過程":0.19827163395829256,"json":0.6423301791599972,"方式":0.582762869780791,"退出應用":1.2338671268242603,"Setup":1.004399549339134}   ["Electron","全自動","產品","版本號","安裝包","檢查更新","方案","版本","退出應用","邏輯","安裝過程","方式","定性","新版本","Setup","靜默","用戶"]
Time taken: 0.322 seconds, Fetched: 1 row(s)
  • 目的:計算文章的關鍵詞以及主題詞結果
    • 關鍵詞:TFIDF/TextRank計算出的結果TOPK個詞以及權重
    • 主題詞:TextRank的TOPK詞 與 ITFDF計算的TOPK個詞的交集
  • 步驟:
    • 1、計算曆史所有文章的TFIDF與TextRank
    • 2、通過TFIDF與TextRank計算關鍵詞與主題詞

2.4.1 離線計算曆史所有文章的TFIDF與TextRank

  • 步驟:
    • 1、原始文章表數據合併得到文章所有的詞語句信息
      • 文章標題+文章頻道名稱+文章內容組成文章完整內容
    • 2、所有歷史文章Tfidf計算
    • 3、所有歷史文章TextRank計算

中間表以及計算結果存儲:

  • 離線文章數量過多,如果一個spark程序完全計算,那麼程序所需內存過大,結果最好中間存儲下來

原始文章數據的合併

爲了方便與進行文章數據操作,將文章相關重要信息表合併在一起。通過spark sql 來進行操作

  • 步驟:
    • 創建Spark初始化相關配置
    • 進行合併計算,合併結果

2.4.1.1 創建Spark初始化相關配置

在_init_文件中,創建一個經常用到的基類

  • 定義好spark啓動的類別,以及相關內存設置
SPARK_APP_NAME = None # APP的名字
SPARK_URL = "yarn" # 啓動運行方式

SPARK_EXECUTOR_MEMORY = "2g" # 執行內存
SPARK_EXECUTOR_CORES = 2 # 每個EXECUTOR能夠使用的CPU core的數量
SPARK_EXECUTOR_INSTANCES = 2 # 最多能夠同時啓動的EXECUTOR的實例個數

ENABLE_HIVE_SUPPORT = False
  • 創建相關配置、包,建立基類
from pyspark import SparkConf
from pyspark.sql import SparkSession
import os


class SparkSessionBase(object):

    SPARK_APP_NAME = None
    SPARK_URL = "yarn"

    SPARK_EXECUTOR_MEMORY = "2g"
    SPARK_EXECUTOR_CORES = 2
    SPARK_EXECUTOR_INSTANCES = 2

    ENABLE_HIVE_SUPPORT = False

    def _create_spark_session(self):

        conf = SparkConf()  # 創建spark config對象
        config = (
            ("spark.app.name", self.SPARK_APP_NAME),  # 設置啓動的spark的app名稱,沒有提供,將隨機產生一個名稱
            ("spark.executor.memory", self.SPARK_EXECUTOR_MEMORY),  # 設置該app啓動時佔用的內存用量,默認2g
            ("spark.master", self.SPARK_URL),  # spark master的地址
            ("spark.executor.cores", self.SPARK_EXECUTOR_CORES),  # 設置spark executor使用的CPU核心數,默認是1核心
            ("spark.executor.instances", self.SPARK_EXECUTOR_INSTANCES)
        )

        conf.setAll(config)

        # 利用config對象,創建spark session
        if self.ENABLE_HIVE_SUPPORT:
            return SparkSession.builder.config(conf=conf).enableHiveSupport().getOrCreate()
        else:
            return SparkSession.builder.config(conf=conf).getOrCreate()

新建一個目錄,用於進行文章內容相關計算目錄,創建reco_sys推薦系統相關計算主目錄:下面建立離線offline以及full_cal

[root@hadoop-master reco_sys]# 
[root@hadoop-master reco_sys]# tree
.
└── offline
    ├── full_cal
    └── __init__.py

2.4.1.2 進行合併計算

由於每次調試運行spark時間較長,我們最終代碼放在pycharm開發目錄中,使用jupyter notebook進行開發

在項目目錄開啓

jupyter notebook --allow-root --ip=192.168.19.137
  • 注意:本地內存不足的情況下,儘量不要同時運行多個Spark APP,否則比如查詢HVIE操作會很慢,磁盤也保證足夠

  • 1、新建文章數據庫,存儲文章數據、中間計算結果以及文章畫像結果
create database if not exists article comment "artcile information" location '/user/hive/warehouse/article.db/';
  • 1、原始文章數據合併表結構,在article數據庫中
    • sentence:文章標題+內容+頻道名字的合併結果
CREATE TABLE article_data(
article_id BIGINT comment "article_id",
channel_id INT comment "channel_id", 
channel_name STRING comment "channel_name",
title STRING comment "title",
content STRING comment "content",
sentence STRING comment "sentence")
COMMENT "toutiao news_channel"
LOCATION '/user/hive/warehouse/article.db/article_data';
hive> select * from article_data limit 1;
OK
1       17      前端    Vue props用法小結原薦   <p><strong>Vue props用法詳解</strong>組件接受的選項之一 props 是 Vue 中非常重要的一個選項。父子組件的關係可以總結爲
  • 3、新建merge_data.ipynb文件
    • 初始化spark信息
import os
import sys
# 如果當前代碼文件運行測試需要加入修改路徑,避免出現後導包問題
BASE_DIR = os.path.dirname(os.path.dirname(os.getcwd()))
sys.path.insert(0, os.path.join(BASE_DIR))
PYSPARK_PYTHON = "/miniconda2/envs/reco_sys/bin/python"
# 當存在多個版本時,不指定很可能會導致出錯
os.environ["PYSPARK_PYTHON"] = PYSPARK_PYTHON
os.environ["PYSPARK_DRIVER_PYTHON"] = PYSPARK_PYTHON
from offline import SparkSessionBase

創建合併文章類,集成sparksessionbase

class OriginArticleData(SparkSessionBase):


    SPARK_APP_NAME = "mergeArticle"
    SPARK_URL = "yarn"

    ENABLE_HIVE_SUPPORT = True

        def __init__(self):
        self.spark = self._create_spark_session()
  • 讀取文章進行處理合並
oa = OriginArticleData()
oa.spark.sql("use toutiao")
# 由於運行速度原因,選擇一篇文章部分數據進行測試
basic_content = oa.spark.sql(
            "select a.article_id, a.channel_id, a.title, b.content from news_article_basic a inner join news_article_content b on a.article_id=b.article_id where a.article_id=116636")

合併數據

import pyspark.sql.functions as F
import gc

# 增加channel的名字,後面會使用
basic_content.registerTempTable("temparticle")
channel_basic_content = oa.spark.sql(
  "select t.*, n.channel_name from temparticle t left join news_channel n on t.channel_id=n.channel_id")

# 利用concat_ws方法,將多列數據合併爲一個長文本內容(頻道,標題以及內容合併)
oa.spark.sql("use article")
sentence_df = channel_basic_content.select("article_id", "channel_id", "channel_name", "title", "content", \
                                           F.concat_ws(
                                             ",",
                                             channel_basic_content.channel_name,
                                             channel_basic_content.title,
                                             channel_basic_content.content
                                           ).alias("sentence")
                                          )
del basic_content
del channel_basic_content
gc.collect()

# sentence_df.write.insertInto("article_data")

運行需要時間等待成功,如果還有其他程序運行,手動釋放內存,避免不夠

del basic_content
del channel_basic_content
gc.collect()

然後執行查詢,看看是否寫入選定的文章

hive> select * from article_data limit 1;

上傳合併完成的所有歷史數據

以上測試在少量數據上進行測試寫入合併文章數據,最終我們拿之前合併的所有文章數據直接上傳到hadoop上使用

hadoop dfs -put /root/bak/hadoopbak/article.db/article_data/ /user/hive/warehouse/article.db/article_data/

2.4.2 Tfidf計算

2.4.2.1 目的

  • 計算出每篇文章的詞語的TFIDF結果

2.4.2.2TFIDF模型的訓練步驟

  • 1、讀取N篇文章數據文章數據進行分詞處理,得到分詞結果
  • 2、TFIDF模型訓練保存,spark使用count與idf進行計算
    • TFIDF計算方案:
    • 先計算分詞之後的每篇文章的詞頻,得到CV模型
    • 然後根據詞頻計算IDF以及詞,得到IDF模型
  • 3、使用模型計算N篇文章數據的TFIDF值

2.4.2.3 實現

想要用TFIDF進行計算,需要訓練一個模型保存結果

  • 新建一個compute_tfidf.ipynb的文件
import os
import sys
# 如果當前代碼文件運行測試需要加入修改路徑,避免出現後導包問題
BASE_DIR = os.path.dirname(os.path.dirname(os.getcwd()))
sys.path.insert(0, os.path.join(BASE_DIR))
PYSPARK_PYTHON = "/miniconda2/envs/reco_sys/bin/python"
# 當存在多個版本時,不指定很可能會導致出錯
os.environ["PYSPARK_PYTHON"] = PYSPARK_PYTHON
os.environ["PYSPARK_DRIVER_PYTHON"] = PYSPARK_PYTHON
from offline import SparkSessionBase

class KeywordsToTfidf(SparkSessionBase):

    SPARK_APP_NAME = "keywordsByTFIDF"
    SPARK_EXECUTOR_MEMORY = "7g"

    ENABLE_HIVE_SUPPORT = True

        def __init__(self):
        self.spark = self._create_spark_session()
ktt = KeywordsToTfidf()
  • 讀取文章原始數據
ktt.spark.sql("use article")
article_dataframe = ktt.spark.sql("select * from article_data limit 20")
words_df = article_dataframe.rdd.mapPartitions(segmentation).toDF(["article_id", "channel_id", "words"])
  • 文章數據處理函數
    • 如果分佈式環境—>結巴詞典,以及停用詞(詞典文件,三臺Centos都需要上傳一份,目錄相同)
    • ITKeywords.txt, stopwords.txt
scp -r ./words/ [email protected]:/root/words/
scp -r ./words/ [email protected]:/root/words/

分詞部分代碼:

# 分詞
def segmentation(partition):
    import os
    import re

    import jieba
    import jieba.analyse
    import jieba.posseg as pseg
    import codecs

    abspath = "/root/words"

    # 結巴加載用戶詞典
    userDict_path = os.path.join(abspath, "ITKeywords.txt")
    jieba.load_userdict(userDict_path)

    # 停用詞文本
    stopwords_path = os.path.join(abspath, "stopwords.txt")

    def get_stopwords_list():
        """返回stopwords列表"""
        stopwords_list = [i.strip()
                          for i in codecs.open(stopwords_path).readlines()]
        return stopwords_list

    # 所有的停用詞列表
    stopwords_list = get_stopwords_list()

    # 分詞
    def cut_sentence(sentence):
        """對切割之後的詞語進行過濾,去除停用詞,保留名詞,英文和自定義詞庫中的詞,長度大於2的詞"""
        # print(sentence,"*"*100)
        # eg:[pair('今天', 't'), pair('有', 'd'), pair('霧', 'n'), pair('霾', 'g')]
        seg_list = pseg.lcut(sentence)
        seg_list = [i for i in seg_list if i.flag not in stopwords_list]
        filtered_words_list = []
        for seg in seg_list:
            # print(seg)
            if len(seg.word) <= 1:
                continue
            elif seg.flag == "eng":
                if len(seg.word) <= 2:
                    continue
                else:
                    filtered_words_list.append(seg.word)
            elif seg.flag.startswith("n"):
                filtered_words_list.append(seg.word)
            elif seg.flag in ["x", "eng"]:  # 是自定一個詞語或者是英文單詞
                filtered_words_list.append(seg.word)
        return filtered_words_list

    for row in partition:
        sentence = re.sub("<.*?>", "", row.sentence)    # 替換掉標籤數據
        words = cut_sentence(sentence)
        yield row.article_id, row.channel_id, words
  • 訓練模型,得到每個文章詞的頻率Counts結果
# 詞語與詞頻統計
from pyspark.ml.feature import CountVectorizer
# 總詞彙的大小,文本中必須出現的次數
cv = CountVectorizer(inputCol="words", outputCol="countFeatures", vocabSize=200*10000, minDF=1.0)
# 訓練詞頻統計模型
cv_model = cv.fit(words_df)
cv_model.write().overwrite().save("hdfs://hadoop-master:9000/headlines/models/CV.model")

  • 訓練idf模型,保存
# 詞語與詞頻統計
from pyspark.ml.feature import CountVectorizerModel
cv_model = CountVectorizerModel.load("hdfs://hadoop-master:9000/headlines/models/CV.model")
# 得出詞頻向量結果
cv_result = cv_model.transform(words_df)
# 訓練IDF模型
from pyspark.ml.feature import IDF
idf = IDF(inputCol="countFeatures", outputCol="idfFeatures")
idfModel = idf.fit(cv_result)
idfModel.write().overwrite().save("hdfs://hadoop-master:9000/headlines/models/IDF.model")

得到了兩個少量文章測試的模型,以這兩個模型爲例子查看結果,所有不同的詞

cv_model.vocabulary
['this',
 'pa',
 'node',
 'data',
 '數據',
 'let',
 'keys',
 'obj',
 '組件',
 'npm',
 'child',
 '節點',
 'log',
 '屬性',
 'key',
 'console',
 'value',
 'var',
 'return',
 'div']

# 每個詞的逆文檔頻率,在歷史13萬文章當中是固定的值,也作爲後面計算TFIDF依據
idfModel.idf.toArray()[:20]
array([0.6061358 , 0.        , 0.6061358 , 0.6061358 , 0.45198512,
       0.78845736, 1.01160091, 1.01160091, 1.01160091, 0.78845736,
       1.29928298, 1.70474809, 0.31845373, 1.01160091, 0.78845736,
       0.45198512, 0.78845736, 0.78845736, 0.45198512, 1.70474809])

上傳訓練13萬文章的模型

  • 20篇文章,計算出代表20篇文章中N個詞的IDF以及每個文檔的詞頻,最終得到的是這20片文章的TFIDF

兩個模型訓練需要很久,所以在這裏我們上傳已經訓練好的模型到指定路徑

hadoop dfs -mkdir /headlines

hadoop dfs -mkdir /headlines/models/

hadoop dfs -put modelsbak/countVectorizerOfArticleWords.model/ /headlines/models/
hadoop dfs -put modelsbak/IDFOfArticleWords.model/ /headlines/models/

最終:

2.4.2.3 計算N篇文章數據的TFIDF值

  • 步驟:
    • 1、獲取兩個模型相關參數,將所有的13萬文章中的關鍵字對應的idf值和索引保存
      • 爲什麼要保存這些值?並且存入數據庫當中?
    • 2、模型計算得出N篇文章的TFIDF值選取TOPK,與IDF索引查詢得到詞

獲取兩個模型相關參數,將所有的關鍵字對應的idf值和索引保存

  • 後續計算tfidf畫像需要使用,避免放入內存中佔用過多,持久化使用

  • 建立表

CREATE TABLE idf_keywords_values(
keyword STRING comment "article_id",
idf DOUBLE comment "idf",
index INT comment "index");

然後讀取13萬參考文檔所有詞語與IDF值進行計算:

from pyspark.ml.feature import CountVectorizerModel
cv_model = CountVectorizerModel.load("hdfs://hadoop-master:9000/headlines/models/countVectorizerOfArticleWords.model")

from pyspark.ml.feature import IDFModel
idf_model = IDFModel.load("hdfs://hadoop-master:9000/headlines/models/IDFOfArticleWords.model")

進行計算保存

#keywords_list_with_idf = list(zip(cv_model.vocabulary, idf_model.idf.toArray()))
#def func(data):
#    for index in range(len(data)):
#    data[index] = list(data[index])
#    data[index].append(index)
#    data[index][1] = float(data[index][1])
#func(keywords_list_with_idf)
#sc = spark.sparkContext
#rdd = sc.parallelize(keywords_list_with_idf)
#df = rdd.toDF(["keywords", "idf", "index"])

# df.write.insertInto('idf_keywords_values')

存儲結果:

hive> select * from idf_keywords_values limit 10;
OK
&#      1.417829594344155       0
pa      0.6651385256756351      1
ul      0.8070591229443697      2
代碼    0.7368239176481552      3
方法    0.7506253985501485      4
數據    0.9375297590538404      5
return  1.1584986818528347      6
對象    1.2765716628665975      7
name    1.3833429138490618      8
this    1.6247297855214076      9

hive> desc idf_keywords_values;
OK
keyword                 string                  article_id          
idf                     double                  idf                 
index                   int                     index

模型計算得出N篇文章的TFIDF值,IDF索引結果合併得到詞

對於詞頻處理之後的結果進行計算

保存TFIDF的結果,在article數據庫中創建表

CREATE TABLE tfidf_keywords_values(
article_id INT comment "article_id",
channel_id INT comment "channel_id",
keyword STRING comment "keyword",
tfidf DOUBLE comment "tfidf");

計算tfidf值進行存儲

from pyspark.ml.feature import CountVectorizerModel
cv_model = CountVectorizerModel.load("hdfs://hadoop-master:9000/headlines/models/countVectorizerOfArticleWords.model")
from pyspark.ml.feature import IDFModel
idf_model = IDFModel.load("hdfs://hadoop-master:9000/headlines/models/IDFOfArticleWords.model")
cv_result = cv_model.transform(words_df)
tfidf_result = idf_model.transform(cv_result)

def func(partition):
    TOPK = 20
    for row in partition:
        # 找到索引與IDF值並進行排序
        _ = list(zip(row.idfFeatures.indices, row.idfFeatures.values))
        _ = sorted(_, key=lambda x: x[1], reverse=True)
        result = _[:TOPK]
        for word_index, tfidf in result:
            yield row.article_id, row.channel_id, int(word_index), round(float(tfidf), 4)

_keywordsByTFIDF = tfidf_result.rdd.mapPartitions(func).toDF(["article_id", "channel_id", "index", "tfidf"])

結果爲:

如果要保存對應words讀取idf_keywords_values表結果合併

# 利用結果索引與”idf_keywords_values“合併知道詞 
keywordsIndex = ktt.spark.sql("select keyword, index idx from idf_keywords_values")
# 利用結果索引與”idf_keywords_values“合併知道詞
keywordsByTFIDF = _keywordsByTFIDF.join(keywordsIndex, keywordsIndex.idx == _keywordsByTFIDF.index).select(["article_id", "channel_id", "keyword", "tfidf"])


# HIVE數據倉庫已經有這個表以及之前計算好的所有頻道的文章結果,這裏做測試就不在進行插入了
# keywordsByTFIDF.write.insertInto("tfidf_keywords_values")

合併之後存儲到TFIDF結果表中,便於後續讀取處理

hive> desc tfidf_keywords_values;
OK
article_id              int                     article_id          
channel_id              int                     channel_id          
keyword                 string                  keyword             
tfidf                   double                  tfidf               
Time taken: 0.085 seconds, Fetched: 4 row(s)
hive> select * from tfidf_keywords_values limit 10;
OK
98319   17      var     20.6079
98323   17      var     7.4938
98326   17      var     104.9128
98344   17      var     5.6203
98359   17      var     69.3174
98360   17      var     9.3672
98392   17      var     14.9875
98393   17      var     155.4958
98406   17      var     11.2407
98419   17      var     59.9502
Time taken: 0.499 seconds, Fetched: 10 row(s)

2.4.3 TextRank提取關鍵詞

要知道TextRank原理,首先介紹PageRank:

來源PageRank算法:

本算法是由谷歌的兩位創始人佩奇(Larry Page)和布林(Sergey Brin)參考學術界評判學術論文重要性的方法———看論文的引用次數 而提出,算法思想:

  • 如果一個網頁被很多其他網頁鏈接到的話說明這個網頁比較重要,也就是PageRank值會相對較高
  • 如果一個PageRank值很高的網頁鏈接到一個其他的網頁,那麼被鏈接到的網頁的PageRank值會相應地因此而提高

  • 算法原理

    • 1、PageRank算法:就是預先給每個網頁一個PR值(下面用PR值指代PageRank值),由於PR值物理意義上爲一個網頁被訪問概率,所以一般是1/N,其中N爲網頁總數(初始概率)。
    • 2、所有網頁的PR值的總和爲1。如果不爲1的話也不是不行,最後算出來的不同網頁之間PR值的大小關係仍然是正確的,只是不能直接地反映概率了。
    • 3、預先給定PR值後,通過下面的算法不斷迭代,直至達到平穩分佈爲止。α參數爲阻尼係數
  • 公式:

  • 其中Mpi是所有對pipi網頁有出鏈的網頁集合,L(pj)是網頁pj的出鏈數目,N是網頁總數,α一般取0.85。

例子:

三個頁面A、B、C爲了便於計算,我們假設每個頁面的PR初始值爲1,α爲0.5

頁面A的PR值計算如下:

PR(A) = 0.5 + 0.5 x PR(C) = 0.5 + 0.5 x 1 = 1

PR(B) = 0.5+0.5 x (PR(A) /2) = 0.5 + 0.5 x (1/ 2) = 0.75

PR(C) = 0.5 + 0.5((PR(A)/2) + PR(B)) = 1.125

……..

下面是迭代計算6輪之後,各個頁面的PR值:

那麼什麼時候,迭代結束哪?一般要設置收斂條件:比如上次迭代結果與本次迭代結果小於某個誤差,我們結束程序運行;比如還可以設置最大循環次數。

2.4.3.1 TextRank 原理

TextRank算法是由PageRank算法改進而來的,二者的思想有相同之處,區別在於:PageRank算法根據網頁之間的鏈接關係構造網絡,而TextRank算法根據詞之間的共現關係構造網絡;PageRank算法構造的網絡中的邊是有向無權邊,而TextRank算法構造的網絡中的邊是無向有權邊。TextRank算法的核心公式如下

TextRank由Mihalcea與Tarau於EMNLP在2014年提出來,其思想非常簡單。關鍵詞抽取的任務就是從一段給定的文本中自動抽取出若干有意義的詞語或詞組。TextRank算法是利用局部詞彙之間關係(共現窗口)對後續關鍵詞進行排序,直接從文本本身抽取

  • 定義:通過詞之間的相鄰關係構建網絡,然後用PageRank迭代計算每個節點的rank值,排序rank值即可得到關鍵詞
  • 方法:無向圖模型進行權重計算
  • 步驟:

注:正規的TextRank公式在PageRank的公式的基礎上,引入了邊的權值的概念

基於TextRank的關鍵詞提取過程步驟如下:

  • 把給定的文本T按照完整句子進行分割,對於每個句子,進行分詞和詞性標註處理,並過濾掉停用詞,只保留指定詞性的單詞,如名詞、動詞、形容詞,即,其中是保留後的候選關鍵詞。
  • 構建候選關鍵詞圖G = (V,E),其中V爲節點集,上一步生成的候選關鍵詞組成,然後採用共現關係(co-occurrence)構造任兩點之間的邊,兩個節點之間存在邊僅當它們對應的詞彙在長度爲K的窗口中共現,K表示窗口大小,即最多共現K個單詞。根據上面公式,迭代傳播各節點的權重,直至收斂。
  • 對節點權重進行倒序排序,從而得到最重要的T個單詞,作爲候選關鍵詞

如:

例如要從下面的文本中提取關鍵詞:

程序員(英文Programmer)是從事程序開發、維護的專業人員。一般將程序員分爲程序設計人員和程序編碼人員,但兩者的界限並不非常清楚,特別是在中國。軟件從業人員分爲初級程序員、高級程序員、系統分析員和項目經理四大類。

1、首先對這句話分詞,這裏可以藉助各種分詞項目,比如HanLP、Jieba分詞,得出分詞結果:

[程序員/n, (, 英文/nz, programmer/en, ), 是/v, 從事/v, 程序/n, 
開發/v, 、/w, 維護/v, 的/uj, 專業/n, 人員/n, 。/w, 一般/a, 
將/d, 程序員/n, 分爲/v, 程序/n, 設計/vn, 人員/n, 和/c, 
程序/n, 編碼/n, 人員/n, ,/w, 但/c, 兩者/r, 的/uj, 界限/n, 
並/c, 不/d, 非常/d, 清楚/a, ,/w, 特別/d, 是/v, 在/p, 中國/ns, 。
/w, 軟件/n, 從業/b, 人員/n, 分爲/v, 初級/b, 程序員/n, 、
/w, 高級/a, 程序員/n, 、/w, 系統/n, 分析員/n, 和/c, 項目/n,
 經理/n, 四/m, 大/a, 類/q, 。/w]

然後去掉裏面的停用詞,這裏我去掉了標點符號、常用詞、以及“名詞、動詞、形容詞、副詞之外的詞”。得出實際有用的詞語:

程序員, 英文, 程序, 開發, 維護, 專業, 人員, 程序員, 分爲, 程序, 設計, 人員, 程序, 編碼, 人員, 界限, 特別, 中國, 軟件, 人員, 分爲, 程序員, 高級, 程序員, 系統, 分析員, 項目, 經理

2、現在建立一個大小爲 9 的窗口,即相當於每個單詞要將票投給它身前身後距離 5 以內的單詞:

開發=[專業, 程序員, 維護, 英文, 程序, 人員]
軟件=[程序員, 分爲, 界限, 高級, 中國, 特別, 人員]
程序員=[開發, 軟件, 分析員, 維護, 系統, 項目, 經理, 分爲, 英文, 程序, 專業, 設計, 高級, 人員, 中國]
分析員=[程序員, 系統, 項目, 經理, 高級]
維護=[專業, 開發, 程序員, 分爲, 英文, 程序, 人員]
系統=[程序員, 分析員, 項目, 經理, 分爲, 高級]
項目=[程序員, 分析員, 系統, 經理, 高級]
經理=[程序員, 分析員, 系統, 項目]
分爲=[專業, 軟件, 設計, 程序員, 維護, 系統, 高級, 程序, 中國, 特別, 人員]
英文=[專業, 開發, 程序員, 維護, 程序]
程序=[專業, 開發, 設計, 程序員, 編碼, 維護, 界限, 分爲, 英文, 特別, 人員]
特別=[軟件, 編碼, 分爲, 界限, 程序, 中國, 人員]
專業=[開發, 程序員, 維護, 分爲, 英文, 程序, 人員]
設計=[程序員, 編碼, 分爲, 程序, 人員]
編碼=[設計, 界限, 程序, 中國, 特別, 人員]
界限=[軟件, 編碼, 程序, 中國, 特別, 人員]
高級=[程序員, 軟件, 分析員, 系統, 項目, 分爲, 人員]
中國=[程序員, 軟件, 編碼, 分爲, 界限, 特別, 人員]
人員=[開發, 程序員, 軟件, 維護, 分爲, 程序, 特別, 專業, 設計, 編碼, 界限, 高級, 中國]

3、然後開始迭代投票,直至收斂:

程序員=1.9249977,
人員=1.6290349,
分爲=1.4027836,
程序=1.4025855,
高級=0.9747374,
軟件=0.93525416,
中國=0.93414587,
特別=0.93352026,
維護=0.9321688,
專業=0.9321688,
系統=0.885048,
編碼=0.82671607,
界限=0.82206935,
開發=0.82074183,
分析員=0.77101076,
項目=0.77101076,
英文=0.7098714,
設計=0.6992446,
經理=0.64640945

可以看到“程序員”的得票數最多,因而它是整段文本最重要的單詞。我們將文本中得票數多的若干單詞作爲該段文本的關鍵詞,若多個關鍵詞相鄰,這些關鍵詞還可以構成關鍵短語。詞性:

2.4.3.1 文章的TextRank計算

  • API:jieba.anlase.textrank

案例:

from jieba import analyse
# 引入TextRank關鍵詞抽取接口
textrank = analyse.textrank

# 原始文本
text = "線程是程序執行時的最小單位,它是進程的一個執行流,\
        是CPU調度和分派的基本單位,一個進程可以由很多個線程組成,\
        線程間共享進程的所有資源,每個線程有自己的堆棧和局部變量。\
        線程由CPU獨立調度執行,在多CPU環境下就允許多個線程同時運行。\
        同樣多線程也可以實現併發操作,每個請求分配一個線程來處理。"

# 基於TextRank算法進行關鍵詞抽取
keywords = textrank(text)
# 輸出抽取出的關鍵詞
for keyword in keywords:
    print(keyword + "/")

spark進行離線計算步驟:

  • 1、TextRank存儲結構
  • 2、TextRank過濾計算

創建textrank_keywords_values表

CREATE TABLE textrank_keywords_values(
article_id INT comment "article_id",
channel_id INT comment "channel_id",
keyword STRING comment "keyword",
textrank DOUBLE comment "textrank");

進行詞的處理與存儲

# 計算textrank
textrank_keywords_df = article_dataframe.rdd.mapPartitions(textrank).toDF(
["article_id", "channel_id", "keyword", "textrank"])

# textrank_keywords_df.write.insertInto("textrank_keywords_values")

分詞結果:

# 分詞
def textrank(partition):
    import os

    import jieba
    import jieba.analyse
    import jieba.posseg as pseg
    import codecs

    abspath = "/root/words"

    # 結巴加載用戶詞典
    userDict_path = os.path.join(abspath, "ITKeywords.txt")
    jieba.load_userdict(userDict_path)

    # 停用詞文本
    stopwords_path = os.path.join(abspath, "stopwords.txt")

    def get_stopwords_list():
        """返回stopwords列表"""
        stopwords_list = [i.strip()
                          for i in codecs.open(stopwords_path).readlines()]
        return stopwords_list

    # 所有的停用詞列表
    stopwords_list = get_stopwords_list()

    class TextRank(jieba.analyse.TextRank):
        def __init__(self, window=20, word_min_len=2):
            super(TextRank, self).__init__()
            self.span = window  # 窗口大小
            self.word_min_len = word_min_len  # 單詞的最小長度
            # 要保留的詞性,根據jieba github ,具體參見https://github.com/baidu/lac
            self.pos_filt = frozenset(
                ('n', 'x', 'eng', 'f', 's', 't', 'nr', 'ns', 'nt', "nw", "nz", "PER", "LOC", "ORG"))

        def pairfilter(self, wp):
            """過濾條件,返回True或者False"""

            if wp.flag == "eng":
                if len(wp.word) <= 2:
                    return False

            if wp.flag in self.pos_filt and len(wp.word.strip()) >= self.word_min_len \
                    and wp.word.lower() not in stopwords_list:
                return True
    # TextRank過濾窗口大小爲5,單詞最小爲2
    textrank_model = TextRank(window=5, word_min_len=2)
    allowPOS = ('n', "x", 'eng', 'nr', 'ns', 'nt', "nw", "nz", "c")

    for row in partition:
        tags = textrank_model.textrank(row.sentence, topK=20, withWeight=True, allowPOS=allowPOS, withFlag=False)
        for tag in tags:
            yield row.article_id, row.channel_id, tag[0], tag[1]

最終存儲爲:

hive> select * from textrank_keywords_values limit 20;
OK
98319   17      var     20.6079
98323   17      var     7.4938
98326   17      var     104.9128
98344   17      var     5.6203
98359   17      var     69.3174
98360   17      var     9.3672
98392   17      var     14.9875
98393   17      var     155.4958
98406   17      var     11.2407
98419   17      var     59.9502
98442   17      var     18.7344
98445   17      var     37.4689
98512   17      var     29.9751
98544   17      var     5.6203
98545   17      var     22.4813
98548   17      var     71.1909
98599   17      var     11.2407
98609   17      var     18.7344
98642   17      var     67.444
98648   15      var     20.6079
Time taken: 0.344 seconds, Fetched: 20 row(s)
hive> desc textrank_keywords_values;
OK
article_id              int                     article_id          
channel_id              int                     channel_id          
keyword                 string                  keyword             
textrank                double                  textrank

2.5.3 文章畫像結果

對文章進行計算畫像

  • 步驟:
    • 1、加載IDF,保留關鍵詞以及權重計算(TextRank * IDF)?爲什麼?合併關鍵詞權重到字典結果
    • 2、將tfidf和textrank共現的詞作爲主題詞
    • 4、將主題詞表和關鍵詞表進行合併,插入表

加載IDF,保留關鍵詞以及權重計算(TextRank * IDF)

idf = ktt.spark.sql("select * from idf_keywords_values")
idf = idf.withColumnRenamed("keyword", "keyword1")
result = textrank_keywords_df.join(idf,textrank_keywords_df.keyword==idf.keyword1)
keywords_res = result.withColumn("weights", result.textrank * result.idf).select(["article_id", "channel_id", "keyword", "weights"])

合併關鍵詞權重到字典結果

keywords_res.registerTempTable("temptable")
merge_keywords = ktt.spark.sql("select article_id, min(channel_id) channel_id, collect_list(keyword) keywords, collect_list(weights) weights from temptable group by article_id")

# 合併關鍵詞權重合併成字典
def _func(row):
    return row.article_id, row.channel_id, dict(zip(row.keywords, row.weights))

keywords_info = merge_keywords.rdd.map(_func).toDF(["article_id", "channel_id", "keywords"])

將tfidf和textrank共現的詞作爲主題詞

topic_sql = """
                select t.article_id article_id2, collect_set(t.keyword) topics from tfidf_keywords_values t
                inner join 
                textrank_keywords_values r
                where t.keyword=r.keyword
                group by article_id2
                """
article_topics = ktt.spark.sql(topic_sql)

將主題詞表和關鍵詞表進行合併

article_profile = keywords_info.join(article_topics, keywords_info.article_id==article_topics.article_id2).select(["article_id", "channel_id", "keywords", "topics"])

# articleProfile.write.insertInto("article_profile")

結果顯示

hive> select * from article_profile limit 1;
OK
26      17      {"策略":0.3973770571351729,"jpg":0.9806348975390871,"用戶":1.2794959063944176,"strong":1.6488457985625076,"文件":0.28144603583387057,"邏輯":0.45256526469610714,"形式":0.4123994242601279,"全自":0.9594604850547191,"h2":0.6244481634710125,"版本":0.44280276959510817,"Adobe":0.8553618185108718,"安裝":0.8305037437573172,"檢查更新":1.8088946300014435,"產品":0.774842382276899,"下載頁":1.4256311032544344,"過程":0.19827163395829256,"json":0.6423301791599972,"方式":0.582762869780791,"退出應用":1.2338671268242603,"Setup":1.004399549339134}   ["Electron","全自動","產品","版本號","安裝包","檢查更新","方案","版本","退出應用","邏輯","安裝過程","方式","定性","新版本","Setup","靜默","用戶"]
Time taken: 0.322 seconds, Fetched: 1 row(s)

2.5.4 總結

  • TFIDF原理以及spark API
  • TextRank原理使用
  • 文章挖掘類標籤的計算關鍵詞,主題詞

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章