PySpark TF-IDF計算(2)

使用PySpark進行TF-IDF計算

這篇博文將記錄使用PySpark進行TF-IDF統計的過程,將提供多種計算方法。

1. 準備數據

爲了簡單,同時爲了驗證自己的程序有木有錯誤,我使用如下的測試數據:

1 我來到北京清華大學
2 他來到了網易杭研大廈
3 我來到北京清華大學
4 他來到了網易杭研大廈
5 我來到北京清華大學,我來到北京清華大學

一共五行,每行代表一篇文章,每行中得文章id和正文使用空格分開,例如第一行:1代表文章id,"我來到北京清華大學"代表一篇文本。
將文本寫入到文件test中。

2. 加載數據並且分詞

分詞采用jieba分詞,代碼如下:

def seg(data):
    """
    分詞後返回分詞的dataframe
    :param spark:
    :param data:
    :return:
    """
    return [w for w in jieba.cut(data.strip(), cut_all=False) if len(w) > 1 and re.match(remove_pattern, w) is not None]

主函數:

if __name__ == '__main__':
    conf = SparkConf().setAppName('text_trans').setMaster("local[*]")
    sc = SparkContext()
    sc.setLogLevel(logLevel='ERROR')
    spark = SparkSession.builder.config(conf=conf).getOrCreate()
	#讀本地文件
    data = spark.read.text('test')
    #使用廣播變量記錄總文本數量
    count = data.count()
    brocast_count = sc.broadcast(count)
	# show 一下數據
    data.show()
    # 根據空格進行文章編號和文本正文的切分,然後對文本正文調用jieba分詞進行分詞
    data = data.rdd.map(lambda x: x[0].split(' '))\
        .map(lambda x: (x[0], x[1], seg(x[1]))) # 分詞

過濾掉空行

如果我們的數據中存在空行或者分詞後去除停用詞後沒有詞了,那麼我們可以把這一行給去掉,spark中去掉空行可以使用filter,我習慣去掉空行的做法是使用一個特殊的符號,然後進行filter,代碼如下:

# 使用-- 標識空行,然後filter 掉這一行的數據,filter中如果改行的邏輯是false那麼就會filter 否則就pass
data = data\
    .map(lambda x: (x[0], '--' if len(x[2]) == 0 else x[1], x[2]))\
    .filter(lambda x: x[1] != '--')

計算TF

TF是針對一篇文章而言的,是一篇文章中得單詞頻率/單詞總數,這裏的計算比較簡單,就不多折騰了。

def calc_tf(line):
    """
    計算每個單詞在每篇文章的tf
    :param line:
    :return:
    """
    cnt_map = {}
    for w in line[2]:
        cnt_map[w] = cnt_map.get(w, 0) + 1
    lens = len(line)
    return [(line[0], (w, cnt *1.0/lens)) for w,cnt in cnt_map.items()]
# 計算tf
tf_data = data.flatMap(calc_tf)

打印可以看到:

print(tf_data.collect())
[('1', ('來到', 0.3333333333333333)), ('1', ('北京', 0.3333333333333333)), ('1', ('清華大學', 0.3333333333333333)), ('2', ('來到', 0.3333333333333333)), ('2', ('網易', 0.3333333333333333)), ('2', ('杭研', 0.3333333333333333)), ('2', ('大廈', 0.3333333333333333)), ('3', ('來到', 0.3333333333333333)), ('3', ('北京', 0.3333333333333333)), ('3', ('清華大學', 0.3333333333333333)), ('4', ('來到', 0.3333333333333333)), ('4', ('網易', 0.3333333333333333)), ('4', ('杭研', 0.3333333333333333)), ('4', ('大廈', 0.3333333333333333)), ('5', ('來到', 0.6666666666666666)), ('5', ('北京', 0.6666666666666666)), ('5', ('清華大學', 0.6666666666666666))]

計算IDF

IDF是逆文檔頻率,表示一個單詞出現在語料庫中出現的頻率,也就是一個單詞在多少篇文章中出現了。
下面就給出二個計算IDF的方法,在計算IDF的時候,flatMap需要帶上文章ID:

  1. 思路1. 分詞後的結果進行flatMap,轉化爲文章ID, 單詞的RDD,然後進行計算,這裏順便了解一下combinedByKey
def flat_with_doc_id(data):
    """
    flat map的時候帶上文章ID
    :param data:
    :return:
    """
    return [(w, data[0]) for w in data[2]]
t = data.flatMap(flat_with_doc_id)\
    .combineByKey(lambda v: 1,
                  lambda x, v: x + 1,
                  lambda x, y: x+y)\
    .map(lambda x: (x[0], x[1]*1.0/brocast_count.value))

下面的代碼打印

print('t', t.collect())

可以看到

t [('來到', 1.2), ('北京', 0.8), ('清華大學', 0.8), ('網易', 0.4), ('杭研', 0.4), ('大廈', 0.4)]

如果沒毛病,計算DF是沒有錯的。

  1. 思路二.考慮到我們最後需要計算TF-IDF,如果第一步計算出TF後再結合這一步計算出來的IDF,那麼就不可避免的進行join操作,這個shuffle非常耗時耗力,我們應該儘量的避免,那麼可不可以通過RDD的transformer計算IDF呢,當然是可以的,下面提供一個我寫的,效率個人覺得還可以,在300W篇文本,單機16G內存在1小時內可以完成。
    def create_combiner(v):
        t = []
        t.append(v)
        return (t, 1)

    def merge(x, v):
        # x==>(list, count)
        t = []
        if x[0] is not None:
            t = x[0]
        t.append(v)
        return (t, x[1] + 1)

    def merge_combine(x, y):
        t1 = []
        t2 = []
        if x[0] is not None:
            t1 = x[0]
        if y[0] is not None:
            t2 = y[0]
        t1 = t1.extend(t2)
        return (t1, x[1] + y[1])

    def flat_map_2(line):
        rst = []
        idf_value = line[1][-1] * 1.0 / brocast_count.value
        for doc_pair in line[1][:-1]:
            print(doc_pair)
            for p in doc_pair:
                rst.append(Row(docId=p[0], token=line[0], tf_value=p[1], idf_value=idf_value, tf_idf_value=p[1] * idf_value))
        return rst

idf_rdd = tf_data.map(lambda x: (x[1][0], (x[0], x[1][1])))\
    .combineByKey(create_combiner,
                   merge,
                   merge_combine)\
    .flatMap(flat_map_2)

將其轉化爲DataFram然後show

    tf_idf_df = spark.createDataFrame(idf_rdd)
    tf_idf_df.show()
    tf_idf_df.printSchema()

可以看到

+-----+---------+-------------------+------------------+-----+
|docId|idf_value|       tf_idf_value|          tf_value|token|
+-----+---------+-------------------+------------------+-----+
|    1|      1.0| 0.3333333333333333|0.3333333333333333|   來到|
|    2|      1.0| 0.3333333333333333|0.3333333333333333|   來到|
|    3|      1.0| 0.3333333333333333|0.3333333333333333|   來到|
|    4|      1.0| 0.3333333333333333|0.3333333333333333|   來到|
|    5|      1.0| 0.6666666666666666|0.6666666666666666|   來到|
|    1|      0.6|0.19999999999999998|0.3333333333333333|   北京|
|    3|      0.6|0.19999999999999998|0.3333333333333333|   北京|
|    5|      0.6|0.39999999999999997|0.6666666666666666|   北京|
|    1|      0.6|0.19999999999999998|0.3333333333333333| 清華大學|
|    3|      0.6|0.19999999999999998|0.3333333333333333| 清華大學|
|    5|      0.6|0.39999999999999997|0.6666666666666666| 清華大學|
|    2|      0.4|0.13333333333333333|0.3333333333333333|   網易|
|    4|      0.4|0.13333333333333333|0.3333333333333333|   網易|
|    2|      0.4|0.13333333333333333|0.3333333333333333|   杭研|
|    4|      0.4|0.13333333333333333|0.3333333333333333|   杭研|
|    2|      0.4|0.13333333333333333|0.3333333333333333|   大廈|
|    4|      0.4|0.13333333333333333|0.3333333333333333|   大廈|
+-----+---------+-------------------+------------------+-----+

如果只是想計算DF,那麼可以直接使用第一個方法,combineByKey比reduceByKey要節省內存消耗,而且在大數據的時候更爲明顯。

完整代碼:

# -*- coding: utf-8 -*-

"""
 計算TF-IDF
 @Time    : 2019/2/18 18:03
 @Author  : MaCan ([email protected])
 @File    : text_transformator.py
"""

from pyspark.sql import SparkSession, Row
from pyspark.sql.types import *
from pyspark import SparkConf, SparkContext

# from spark_work.io_utils import mrp_hdfs_2016_path,mrp_hdfs_2017_path,mrp_hdfs_2018_path, user_dict_path

import jieba
import os
import re

#過濾英文的pattern
remove_pattern = '[\u4e00-\u9fa5]+'


# if os.path.exists(user_dict_path):
#     try:
#         jieba.load_userdict(user_dict_path)
#     except Exception as e:
#         print(e)


def seg(data):
    """
    分詞後返回分詞的dataframe
    :param spark:
    :param data:
    :return:
    """
    return [w for w in jieba.cut(data.strip(), cut_all=False) if len(w) > 1 and re.match(remove_pattern, w) is not None]


def flat_with_doc_id(data):
    """
    flat map的時候帶上文章ID
    :param data:
    :return:
    """
    return [(data[0], w) for w in data[2]]


def calc_tf(line):
    """
    計算每個單詞在每篇文章的tf
    :param line:
    :return:
    """
    cnt_map = {}
    for w in line[2]:
        cnt_map[w] = cnt_map.get(w, 0) + 1
    lens = len(line)
    return [(line[0], (w, cnt *1.0/lens)) for w,cnt in cnt_map.items()]

def create_combiner(v):
    t = []
    t.append(v)
    return (t, 1)

def merge(x, v):
    # x==>(list, count)
    t = []
    if x[0] is not None:
        t = x[0]
    t.append(v)
    return (t, x[1] + 1)

def merge_combine(x, y):
    t1 = []
    t2 = []
    if x[0] is not None:
        t1 = x[0]
    if y[0] is not None:
        t2 = y[0]
    t1 = t1.extend(t2)
    return (t1, x[1] + y[1])

def flat_map_2(line):
    rst = []
    idf_value = line[1][-1] * 1.0 / brocast_count.value
    for doc_pair in line[1][:-1]:
        print(doc_pair)
        for p in doc_pair:
            rst.append(Row(docId=p[0], token=line[0], tf_value=p[1], idf_value=idf_value, tf_idf_value=p[1] * idf_value))
    return rst


if __name__ == '__main__':
    conf = SparkConf().setAppName('text_trans').setMaster("local[*]")
    sc = SparkContext()
    sc.setLogLevel(logLevel='ERROR')
    spark = SparkSession.builder.config(conf=conf).getOrCreate()

    data = spark.read.text('test')
    count = data.count()
    brocast_count = sc.broadcast(count)

    data.show()
    data = data.rdd.map(lambda x: x[0].split(' '))\
        .map(lambda x: (x[0], x[1], seg(x[1]))) # 分詞

    # filter掉返回false的結果
    data = data\
        .map(lambda x: (x[0], '--' if len(x[2]) == 0 else x[1], x[2]))\
        .filter(lambda x: x[1] != '--')
    print('data', data.collect())
    data.cache() # x[0]=> docId x[1]==> token
    # 算tf
    tf_data = data.flatMap(calc_tf)
    print(tf_data.collect())


    #idf
    t = data.flatMap(flat_with_doc_id)\
        .map(lambda x: (x[1], x[0]))\
        .combineByKey(lambda v: 1,
                      lambda x, v: x + 1,
                      lambda x, y: x+y)\
        .map(lambda x: (x[0], x[1]*1.0/brocast_count.value)).collect()
    print('t', t)
    print('*'*20)

    idf_rdd = tf_data.map(lambda x: (x[1][0], (x[0], x[1][1])))\
        .combineByKey(create_combiner,
                       merge,
                       merge_combine)\
        .flatMap(flat_map_2)

    tf_idf_df = spark.createDataFrame(idf_rdd)
    tf_idf_df.show()
    tf_idf_df.printSchema()
    spark.stop()


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