程世東老師TensorFlow實戰——個性化推薦,程式碼學習筆記之資料匯入&資料預處理(上)
阿新 • • 發佈:2018-11-27
程式碼來自於知乎: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'))