1. 程式人生 > >程世東老師TensorFlow實戰——個性化推薦,程式碼學習筆記之資料匯入&資料預處理(上)

程世東老師TensorFlow實戰——個性化推薦,程式碼學習筆記之資料匯入&資料預處理(上)

程式碼來自於知乎:https://zhuanlan.zhihu.com/p/32078473

/程式碼地址https://github.com/chengstone/movie_recommender/blob/master/movie_recommender.ipynb

下一篇有一些資料的視覺化,幫助理解

#執行下面程式碼把資料下載下來
import pandas as pd   
from sklearn.model_selection import train_test_split #資料集劃分訓練集和測試集
import numpy as np
from collections import Counter #counter用於統計字元出現的次數
import tensorflow as tf

import os  #os 模組提供了非常豐富的方法用來處理檔案和目錄
import pickle #提供儲存資料在本地的方法
import re #正則表示式
from tensorflow.python.ops import math_ops
from urllib.request import urlretrieve #將URL表示的網路物件複製到本地檔案
from os.path import isfile, isdir #判斷是否存在檔案file,資料夾dir
from tqdm import tqdm #Tqdm 是一個快速,可擴充套件的Python進度條,可以在 Python 長迴圈中新增一個進度提示資訊,使用者只需要封裝任意的迭代器 tqdm(iterator)
import zipfile #用來做zip格式編碼的壓縮和解壓縮的
import hashlib #用來進行hash 或者md5 加密

def _unzip(save_path, _, database_name, data_path):
    """
    Unzip wrapper with the same interface as _ungzip使用與_ungzip相同的介面解壓縮包裝器
    :param save_path: gzip檔案的路徑
    :param database_name:資料庫的名稱
    :param data_path: 提取路徑
    :param _: HACK - Used to have to same interface as _ungzip 用於與_ungzip具有相同的介面??解壓後的檔案路徑
    """
    print('Extracting {}...'.format(database_name))#.format通過 {} 來代替字串database_name
    with zipfile.ZipFile(save_path) as zf: #ZipFile是zipfile包中的一個類,用來建立和讀取zip檔案
        zf.extractall(data_path) #類函式zipfile.extractall([path[, member[, password]]]) 
                                 #path解壓縮目錄,沒什麼可說的 
                                 # member需要解壓縮的檔名列表 
                                 # password當zip檔案有密碼時需要該選項 

def download_extract(database_name, data_path):
    """
    下載並提取資料庫
    :param database_name: Database name
    data_path 這裡為./表示當前目錄
    save_path 下載後資料的儲存路徑即壓縮檔案的路徑
    extract_path 解壓後的檔案路徑
    """
    DATASET_ML1M = 'ml-1m'

    if database_name == DATASET_ML1M:
        url = 'http://files.grouplens.org/datasets/movielens/ml-1m.zip'
        hash_code = 'c4d9eecfca2ab87c1945afe126590906'
        extract_path = os.path.join(data_path, 'ml-1m')#os.path.join將多個路徑組合後返回,提取資料的路徑
        save_path = os.path.join(data_path, 'ml-1m.zip')#要儲存的路徑
        extract_fn = _unzip 

    if os.path.exists(extract_path):  #指定路徑(檔案或者目錄)是否存在
        print('Found {} Data'.format(database_name))
        return

    if not os.path.exists(data_path):  #指定路徑(檔案或者目錄)不存在,則遞迴建立目錄data_path
        os.makedirs(data_path) 

    if not os.path.exists(save_path): #指定路徑(檔案或者目錄)不存在,則遞迴建立目錄save_path
        with DLProgress(unit='B', unit_scale=True, miniters=1, desc='Downloading {}'.format(database_name)) as pbar:#呼叫類,進度條顯示相關,tqdm相關引數設定
            urlretrieve(url, save_path, pbar.hook) #urlretrieve()方法直接將遠端資料下載到本地 rlretrieve(url, filename=None, reporthook=None, data=None)
                                                    #filename指定了儲存本地路徑
                                                    #reporthook是回撥函式,當連線上伺服器、以及相應的資料塊傳輸完畢時會觸發該回調,可利用回撥函式顯示當前下載進度。


    assert hashlib.md5(open(save_path, 'rb').read()).hexdigest() == hash_code, \
        '{} file is corrupted.  Remove the file and try again.'.format(save_path)
     #assert expression [, arguments]表示斷言測試,如expression異常,則輸出後面字串資訊
     #能指出資料是否被篡改過,就是因為摘要函式是一個單向函式,計算f(data)很容易,但通過digest反推data卻非常困難。而且,對原始資料做一個bit的修改,都會導致計算出的摘要完全不同。
     #摘要演算法應用:使用者儲存使用者名稱密碼,但在資料庫不能以明文儲存,而是用md5,當一個使用者輸入密碼時,進行md5匹配,如果相同則可以登入
     #hashlib提供了常見的摘要演算法,如MD5,SHA1等等。摘要演算法又稱雜湊演算法、雜湊演算法。它通過一個函式,把任意長度的資料轉換為一個長度固定的資料串(通常用16進位制的字串表示)。
     #hexdigest為md5後的結果

    os.makedirs(extract_path)
    try:
        extract_fn(save_path, extract_path, database_name, data_path)#解壓
    except Exception as err:
        shutil.rmtree(extract_path)  # Remove extraction folder if there is an error表示遞迴刪除資料夾下的所有子資料夾和子檔案
        raise err#丟擲異常

    print('Done.')
    # Remove compressed data
#     os.remove(save_path)

class DLProgress(tqdm):
    """
    Handle Progress Bar while Downloading下載時處理進度條
    """
    last_block = 0

    def hook(self, block_num=1, block_size=1, total_size=None):
        """
        該函式在建立網路連線時呼叫一次,之後在每個塊讀取後呼叫一次.
        :param block_num: 到目前為止轉移的塊數
        :param block_size: 塊大小(位元組)
        :param total_size: 檔案的總大小。 對於較舊的FTP伺服器,這可能為-1,該伺服器不響應檢索請求而返回檔案大小。
        """
        self.total = total_size
        self.update((block_num - self.last_block) * block_size)
        self.last_block = block_num
data_dir = './'
download_extract('ml-1m', data_dir)

#------------------------------------------------------------------------------
#實現資料預處理
def load_data():
    """
    Load Dataset from File
    """
    #讀取User資料-------------------------------------------------------------
    users_title = ['UserID', 'Gender', 'Age', 'JobID', 'Zip-code']
    users = pd.read_table('./ml-1m/users.dat', sep='::', header=None, names=users_title, engine = 'python')
                                                #分隔符引數:sep
                                                #是否讀取文字資料的header,headers = None表示使用預設分配的列名,一般用在讀取沒有header的資料檔案
                                                #為文字的資料加上自定義列名: names
                                                #pandas.read_csv()從檔案,URL,檔案型物件中載入帶分隔符的資料。預設分隔符為','
                                                #pandas.read_table()從檔案,URL,檔案型物件中載入帶分隔符的資料。預設分隔符為'\t'
    
    users = users.filter(regex='UserID|Gender|Age|JobID')
                                                #DataFrame.filter(items=None, like=None, regex=None, axis=None)
                                                #這裡使用正則式進行過濾
    users_orig = users.values #dataframe.values以陣列的形式返回DataFrame的元素
    
    #改變User資料中性別和年齡
    gender_map = {'F':0, 'M':1}
    users['Gender'] = users['Gender'].map(gender_map) #map()函式可以用於Series物件或DataFrame物件的一列,接收函式或字典物件作為引數,返回經過函式或字典對映處理後的值。

    age_map = {val:ii for ii,val in enumerate(set(users['Age']))}
                                                #enumerate() 函式用於將一個可遍歷的資料物件(如列表、元組或字串)組合為一個索引序列
                                                #同時列出資料和資料下標,一般用在 for 迴圈當中
                                                #set() 函式建立一個無序不重複元素集
    users['Age'] = users['Age'].map(age_map) #map接收的引數是函式

    #讀取Movie資料集---------------------------------------------------------
    movies_title = ['MovieID', 'Title', 'Genres']
    movies = pd.read_table('./ml-1m/movies.dat', sep='::', header=None, names=movies_title, engine = 'python')
    movies_orig = movies.values
    #將Title中的年份去掉
    pattern = re.compile(r'^(.*)\((\d+)\)$')    #re.compile(strPattern[, flag]):把正則表示式的模式和標識轉化成正則表示式物件。供match()和search()這兩個函式使用
                                                #第二個引數flag是匹配模式,取值可以使用按位或運算子'|'表示同時生效
                                                #r表示後面是一個正則表示式''
                                                #^匹配開頭,$匹配結尾,(.*)中的()表示匹配其中的任意正則表示式,.匹配任何字元,*代表可以重複0次或多次
                                                #\(和\):表示對括號的轉義,匹配文字中真正的括號
                                                #(\d+)表示匹配()內的任意字元,\d表示任何數字,+代表數字重複一次或者多次
                                                
    title_map = {val:pattern.match(val).group(1) for ii,val in enumerate(set(movies['Title']))}
                                                #這裡的ii是索引值,val是真正的列表中Title元素
                                                #pattern.match(val)使用Pattern匹配文字val,獲得匹配結果,無法匹配時將返回None
                                                #group獲得一個或多個分組截獲的字串;指定多個引數時將以元組形式返回,分組是按照()匹配順序進行
                                                #這裡group(1)相當於只返回第一組,分組標號從1開始。不填則為返回全部結果
                                                #這裡即完成了將電影名稱的時間去掉
    movies['Title'] = movies['Title'].map(title_map)#title列的電影名轉化為去掉名稱後的電影名


    #電影Title轉數字字典
    title_set = set() #set() 函式建立一個無序不重複元素集,返回一個可迭代物件
    for val in movies['Title'].str.split():#對於電影名稱按空格分,val為整個電影列表中全部單詞
                                            #注意string.split() 不帶引數時,和 string.split(' ') 是有很大區別的
                                            #不帶引數的不會截取出空格,而帶引數的只按一個空格去切分,多出來的空格會被截取出來
                                            #參見https://code.ziqiangxuetang.com/python/att-string-split.html
        title_set.update(val)#新增新元素到集合當中,即完成出現電影中的新單詞時,存下來
    
    title_set.add('<PAD>')#這裡不是numpy.pad函式,只是一個填充表示,也為<PAD>進行編碼
    title2int = {val:ii for ii, val in enumerate(title_set)}#為全部單詞進行像字典一樣進行標註'描述電影的word:數字'格式,即數字字典

    #將電影Title轉成等長數字列表,長度是15
    title_count = 15
    title_map = {val:[title2int[row] for row in val.split()] for ii,val in enumerate(set(movies['Title']))}
                                #for ii,val in enumerate(set(movies['Title']))得到ii索引值和其對應的不重複的一個電影字串val(去掉月份的)
                                #val.split()得到全部被空格分開的電影名稱字串列表,row遍歷電影集中一個電影的全部單詞
                                #title_map得到的是字典,格式為'一個電影字串:[描述這個電影的全部單詞構成的一個對應的數值列表]'
    for key in title_map:
        for cnt in range(title_count - len(title_map[key])):
            title_map[key].insert(len(title_map[key]) + cnt,title2int['<PAD>'])#insert(index, object) 在指定位置index前插入元素object
                                 #index                    ,object  電影key長度少於15就加填充符  
    
    movies['Title'] = movies['Title'].map(title_map)#title欄位的去掉名稱後的電影名轉化為對應的數字列表
                                            #如電影集中的一行資料如下movieid,title,genre
                                            # array([1,
                                               # list([3001, 5100, 275, 275, 275, 275, 275, 275, 275, 275, 275, 275, 275, 275, 275]),
                                               # list([3, 6, 2, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17])], dtype=object)


    #電影型別轉數字字典
    genres_set = set()  
    for val in movies['Genres'].str.split('|'):#對於一個電影的題材進行字串轉化,並用|分割遍歷
        genres_set.update(val)  #set.update()方法用於修改當前集合,可以新增新的元素或集合到當前集合中,如果新增的元素在集合中已存在,則該元素只會出現一次,重複的會忽略。
                                #將描述不同題材的電影存入set
    genres_set.add('<PAD>') #集合add方法:是把要傳入的元素做為一個整個新增到集合中,為<PAD>進行編碼
    genres2int = {val:ii for ii, val in enumerate(genres_set)}#將型別轉化為'字串:數字'格式,即數字字典,同上面電影名稱,一個word對應一個數字
                                                              #而一個電影由多個word構成

    #將電影型別轉成等長數字列表,長度是18
    genres_map = {val:[genres2int[row] for row in val.split('|')] for ii,val in enumerate(set(movies['Genres']))}

    for key in genres_map:
        for cnt in range(max(genres2int.values()) - len(genres_map[key])):
            genres_map[key].insert(len(genres_map[key]) + cnt,genres2int['<PAD>'])
    
    movies['Genres'] = movies['Genres'].map(genres_map)


    #讀取評分資料集--------------------------------------------------------
    ratings_title = ['UserID','MovieID', 'ratings', 'timestamps']
    ratings = pd.read_table('./ml-1m/ratings.dat', sep='::', header=None, names=ratings_title, engine = 'python')
    ratings = ratings.filter(regex='UserID|MovieID|ratings')

    #合併三個表
    data = pd.merge(pd.merge(ratings, users), movies)#通過一個或多個鍵將兩個資料集的行連線起來,類似於 SQL 中的 JOIN
                                                    #合併左dataframe和右datafram,預設為取交集,取交集作為索引鍵

    
    #將資料分成X和y兩張表
    target_fields = ['ratings']
    features_pd, targets_pd = data.drop(target_fields, axis=1), data[target_fields]
                    #features_pd只刪除rating作為x表;targets_pd只有rating作為y
                    #刪除表中的某一行或者某一列使用drop,不改變原有的df中的資料,而是返回另一個dataframe來存放刪除後的資料。
    
    features = features_pd.values
    targets_values = targets_pd.values
    
    return title_count, title_set, genres2int, features, targets_values, ratings, users, movies, data, movies_orig, users_orig
    #title_count電影名長度15
    #title_set {索引:去掉年份且不重複的電影名}
    #genres2int {題材字串列表:數字}
    #features 去掉評分ratings列的三表合併資訊,作為輸入x。則列資訊:userid,gender,age,occupation,movieid,title,genres
    #targets_values 評分,學習目標y,三表合併後的對應ratings
    #返回處理後的ratings,users,movies表,pandas物件
    #返回三表的合併表data
    #moives表中的原始資料值:movies_orig
    #users表中的原始資料值:users_orig
#---------------------------------------------------------------------------
#載入資料並儲存到本地
title_count, title_set, genres2int, features, targets_values, ratings, users, movies, data, movies_orig, users_orig = load_data()
pickle.dump((title_count, title_set, genres2int, 
                features, targets_values, ratings, 
                users, movies, data, movies_orig, users_orig), open('preprocess.p', 'wb'))
                #pickle.dump(obj, file[, protocol])序列化物件,並將結果資料流寫入到檔案物件中
                #儲存資料到本地,便於後續的提取,以防後面用到這些資料還要進行一邊上次的資料預處理過程
#從本地讀取資料,下面這些程式碼可以用在核心程式碼中第一步資料讀取
title_count, title_set, genres2int, features, targets_values, ratings, users, movies, data, movies_orig, users_orig = pickle.load(open('preprocess.p', mode='rb'))