1. 程式人生 > >程世東老師TensorFlow實戰——個性化推薦,程式碼學習筆記之②模型訓練與測試

程世東老師TensorFlow實戰——個性化推薦,程式碼學習筆記之②模型訓練與測試

個性化推薦第二部分:模型訓練

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

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

前面資料預處理已經將訓練好的資料儲存在proprecess.p檔案中,這裡直接引用(從本地讀取)即可,不用每次都跑一遍模型預處理過程

具體描述過程請參加原博(上面連結)

import tensorflow as tf
import os
import pickle

import matplotlib.pyplot as plt
import time
import datetime

def save_params(params):
    """
    Save parameters to file
    """
    pickle.dump(params, open('params.p', 'wb'))


def load_params():
    """
    Load parameters from file
    """
    return pickle.load(open('params.p', mode='rb'))
    
#從本地讀取資料
title_count, title_set, genres2int, features, targets_values, ratings, users, movies, data, movies_orig, users_orig = pickle.load(open('preprocess.p', mode='rb'))

    
#嵌入矩陣的維度:一個單詞或其他變數的特徵表示
embed_dim = 32
                                        #features為
                                        # [ [1, 1193, 0, ..., 10,list([ title]),list([ genres])],
                                        #   [2, 1193, 1, ..., 16,list([ ]),list([ ])],
                                        #   [12, 1193, 1, ..., 12,list([ ]),list([ ])],
                                        #   ..., 
                                        #   [5938, 2909, 1, ..., 1,list([ ]),list([ ])] 
                                        # ]

#使用者ID個數
uid_max = max(features.take(0,1)) + 1 # 6040
                                      #features.take(0,1)得到userid的全部列,由於從0開始編號,則max取最大值再加1可以得到使用者id個數
                                      #ndarray.take(indices, axis=None, out=None, mode='raise')從軸axis上獲取陣列中的元素,並以一維陣列或者矩陣返回
                                      #按axis選擇處於indices位置上的值
                                      #axis用於選擇值的軸,0為橫軸,1為縱向選
                                      #如features.take(0,0)就會選擇橫向第一條資料,(1,0)會選擇橫向第二條資料
#性別個數
gender_max = max(features.take(2,1)) + 1 # 1 + 1 = 2
#年齡類別個數
age_max = max(features.take(3,1)) + 1 # 6 + 1 = 7
#職業個數
job_max = max(features.take(4,1)) + 1# 20 + 1 = 21
#電影ID個數
movie_id_max = max(features.take(1,1)) + 1 # 3952
#電影型別個數
movie_categories_max = max(genres2int.values()) + 1 # 18 + 1 = 19
#電影名單詞個數
movie_title_max = len(title_set) # 5216
                                 # title_set是由空格分開的電影單詞字串構成的列表(set表)

#對電影型別嵌入向量做加和操作的標誌,後面呼叫combiner來使用作為引數
combiner = "sum"

#電影名長度
sentences_size = title_count #  title_count=15重新命名,一個電影title欄位的長度,不夠會補
#文字卷積滑動視窗,分別滑動2, 3, 4, 5個單詞
window_sizes = {2, 3, 4, 5}
#文字卷積核數量
filter_num = 8

#電影ID轉下標的字典,注意資料集中電影ID跟下標不一致,比如第5行的資料電影ID不一定是5
movieid2idx = {val[0]:i for i, val in enumerate(movies.values)} #格式為{movieid :i}
                                                                #1:0,2:1,...

#超參-----------------------------------------------
# Number of Epochs
num_epochs = 5
# Batch Size
batch_size = 256

dropout_keep = 0.5
# Learning Rate
learning_rate = 0.0001
# Show stats for every n number of batches
show_every_n_batches = 20
#後面要儲存模型的地址引數賦給變數save_dir
save_dir = './save'

#輸入-----------------------------------------------
#定義輸入的佔位符
def get_inputs():
    #tf.placeholder(dtype, shape=None, name=None)此函式可以理解為形參,用於定義過程,在執行的時候再賦具體的值
    #dtype:資料型別
    #shape:資料形狀。預設是None,[行數,列數]
    #name:名稱
    uid = tf.placeholder(tf.int32, [None, 1], name="uid") #這裡一行代表一個使用者的id,是batch×1,每一行是一個列表
    user_gender = tf.placeholder(tf.int32, [None, 1], name="user_gender")
    user_age = tf.placeholder(tf.int32, [None, 1], name="user_age")
    user_job = tf.placeholder(tf.int32, [None, 1], name="user_job")
    
    movie_id = tf.placeholder(tf.int32, [None, 1], name="movie_id")
    movie_categories = tf.placeholder(tf.int32, [None, 18], name="movie_categories")
    movie_titles = tf.placeholder(tf.int32, [None, 15], name="movie_titles")
    
    targets = tf.placeholder(tf.int32, [None, 1], name="targets")
    LearningRate = tf.placeholder(tf.float32, name = "LearningRate")
    dropout_keep_prob = tf.placeholder(tf.float32, name = "dropout_keep_prob")
    return uid, user_gender, user_age, user_job, movie_id, movie_categories, movie_titles, targets, LearningRate, dropout_keep_prob

#構建神經網路---------------------------------------
#定義User的嵌入矩陣,完成原始矩陣經過嵌入層後得到的輸出
def get_user_embedding(uid, user_gender, user_age, user_job): #見作者結構圖,使用者嵌入矩陣輸入4個
    with tf.name_scope("user_embedding"):    #用於後面tensorboard視覺化圖層關係
        #使用者id
        uid_embed_matrix = tf.Variable(tf.random_uniform([uid_max, embed_dim], -1, 1), name = "uid_embed_matrix")
                            #tf.Variable(initializer,name):initializer是初始化引數,可以有tf.random_normal,tf.constant等,name是變數的名字
                            #tf.random_uniform(shape, minval=0,maxval=None,dtype=tf.float32) 從均勻分佈中輸出隨機值。
                            #返回shape形狀矩陣:使用者數×特徵數,產生於low(-1)和high(1)之間,產生的值是均勻分佈的。
        uid_embed_layer = tf.nn.embedding_lookup(uid_embed_matrix, uid, name = "uid_embed_layer")
                            #tf.nn.embedding_lookup(tensor, id) 選取一個張量tensor裡面索引id對應的元素
                            #選取uid_embed_matrix的使用者id對應的某個使用者id的向量
        
        #性別
        gender_embed_matrix = tf.Variable(tf.random_uniform([gender_max, embed_dim // 2], -1, 1), name= "gender_embed_matrix")
                            #這裡特徵數降一半
        gender_embed_layer = tf.nn.embedding_lookup(gender_embed_matrix, user_gender, name = "gender_embed_layer")
                            #選取gender_embed_matrix的使用者性別對應的某個使用者性別的向量
        
        #年齡
        age_embed_matrix = tf.Variable(tf.random_uniform([age_max, embed_dim // 2], -1, 1), name="age_embed_matrix")
        age_embed_layer = tf.nn.embedding_lookup(age_embed_matrix, user_age, name="age_embed_layer")
                            #選取age_embed_matrix的使用者年齡對應的某個使用者年齡的向量
        
        #job
        job_embed_matrix = tf.Variable(tf.random_uniform([job_max, embed_dim // 2], -1, 1), name = "job_embed_matrix")
        job_embed_layer = tf.nn.embedding_lookup(job_embed_matrix, user_job, name = "job_embed_layer")
                            #選取job_embed_matrix的使用者job對應的某個使用者job的向量
    return uid_embed_layer, gender_embed_layer, age_embed_layer, job_embed_layer 
#將User的嵌入矩陣一起全連線生成User的特徵
def get_user_feature_layer(uid_embed_layer, gender_embed_layer, age_embed_layer, job_embed_layer):
    with tf.name_scope("user_fc"):
        #第一層全連線
        uid_fc_layer = tf.layers.dense(uid_embed_layer, embed_dim, name = "uid_fc_layer", activation=tf.nn.relu)
        gender_fc_layer = tf.layers.dense(gender_embed_layer, embed_dim, name = "gender_fc_layer", activation=tf.nn.relu)
        age_fc_layer = tf.layers.dense(age_embed_layer, embed_dim, name ="age_fc_layer", activation=tf.nn.relu)
        job_fc_layer = tf.layers.dense(job_embed_layer, embed_dim, name = "job_fc_layer", activation=tf.nn.relu)
                        # tf.layers.dense(inputs,units,activation=None,use_bias=True,kernel_initializer=None,bias_initializer=tf.zeros_initializer(),
                        #                                kernel_regularizer=None,bias_regularizer=None,activity_regularizer=None,kernel_constraint=None,
                        #                                bias_constraint=None,trainable=True,name=None,reuse=None)
                        #inputs:該層的輸入; units:輸出的大小(維數),整數或long; activation: 使用什麼啟用函式(神經網路的非線性層),預設為None,不使用啟用函式
                        #name該層的名字

        
        #第二層全連線
        user_combine_layer = tf.concat([uid_fc_layer, gender_fc_layer, age_fc_layer, job_fc_layer], 2)  #(?, 1, 128)
                                #tf.concat(values,axis,name)是連線兩個矩陣的操作,本身不會增加維度,返回的是連線後的tensor.
                                #values應該是一個tensor的list或者tuple。
                                #axis則是我們想要連線的維度。對於二維來說0表示第一個括號維度,1表示第二個括號維度
                                                       #t1 = [[1, 2, 3], [4, 5, 6]]
                                                       #t2 = [[7, 8, 9], [10, 11, 12]]
                                                       #tf.concat([t1, t2], 0)表示為 [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
                                                       #tf.concat([t1, t2], 1)表示為 [[1, 2, 3, 7, 8, 9], [4, 5, 6, 10, 11, 12]]
                                                       #     對於三維來說0表示第一個括號維度,1表示第二個括號維度,2表示第三個括號維度
                                                       #t1 = [ [[1],[2]] , [[3],[4]] ]
                                                       #t2 = [ [[5],[6]] , [[7],[8]] ]
                                                       #tf.concat([t1, t2], 0)表示為[[[1],[2]] , [[3],[4]] , [[5],[6]] , [[7],[8]]]
                                                       #tf.concat([t1, t2], 1)表示為[[[1],[2],[5],[6]],[[3],[4],[7],[8]]]
                                                       #tf.concat([t1, t2], 2)表示為[[[1 5],[2 6]],[[3 7],[4 8]]]


        user_combine_layer = tf.contrib.layers.fully_connected(user_combine_layer, 200, tf.tanh)  #(?, 1, 200)
                                #tf.contrib.layers.fully_connected(F輸入, num_outputs,activation_fn)用於構建全連線層
    
        user_combine_layer_flat = tf.reshape(user_combine_layer, [-1, 200]) #-1表示預設值,滿足其他維度要求,這裡該是幾就是幾,(?, 200)
    return user_combine_layer, user_combine_layer_flat

#對電影型別的多個嵌入向量做加和
def get_movie_categories_layers(movie_categories):
    with tf.name_scope("movie_categories_layers"):
        movie_categories_embed_matrix = tf.Variable(tf.random_uniform([movie_categories_max, embed_dim], -1, 1), name = "movie_categories_embed_matrix")
        movie_categories_embed_layer = tf.nn.embedding_lookup(movie_categories_embed_matrix, movie_categories, name = "movie_categories_embed_layer")#(?,18,32)
        if combiner == "sum":
            movie_categories_embed_layer = tf.reduce_sum(movie_categories_embed_layer, axis=1, keep_dims=True)#(?,1,32)
                                            #tf.reduce_sum(input_tensor,axis=None,keep_dims=False,name=None,reduction_indices=None)要輸出看!
                                            #壓縮求和,用於降維
                                            #函式中的input_tensor是按照axis中已經給定的維度來減少的,axis表示按第幾個維度求和
                                            #但是keep_dims為true,則維度不會減小
                                            #如果axis沒有條目,則縮小所有維度,並返回具有單個元素的張量
                                            #'x' is [[1, 1, 1]
                                            #        [1, 1, 1]]
                                            #求和tf.reduce_sum(x) ==> 6
                                            #按列求和tf.reduce_sum(x, 0) ==> [2, 2, 2](即得到行結果)
                                            #按行求和tf.reduce_sum(x, 1) ==> [3, 3] (即得到列結果)
                                            #按照行的維度求和tf.reduce_sum(x, 1, keep_dims=True) ==> [[3], [3]](維度不會減少)
                                            #x現在一行為一個型別向量,應該按axis=1進行sum
    #     elif combiner == "mean":
    return movie_categories_embed_layer

#定義Movie ID的嵌入矩陣
def get_movie_id_embed_layer(movie_id):
    with tf.name_scope("movie_embedding"):
        movie_id_embed_matrix = tf.Variable(tf.random_uniform([movie_id_max, embed_dim], -1, 1), name = "movie_id_embed_matrix")
        movie_id_embed_layer = tf.nn.embedding_lookup(movie_id_embed_matrix, movie_id, name = "movie_id_embed_layer")
    return movie_id_embed_layer

#Movie Title的文字卷積網路實現
def get_movie_cnn_layer(movie_titles):
    #從嵌入矩陣中得到電影名對應的各個單詞的嵌入向量
    with tf.name_scope("movie_embedding"):
        movie_title_embed_matrix = tf.Variable(tf.random_uniform([movie_title_max, embed_dim], -1, 1), name = "movie_title_embed_matrix")
        movie_title_embed_layer = tf.nn.embedding_lookup(movie_title_embed_matrix, movie_titles, name = "movie_title_embed_layer")
        movie_title_embed_layer_expand = tf.expand_dims(movie_title_embed_layer, -1)#(?,15,32,1)
                                        #tf.expand_dims(input, axis, name=None)在axis軸,維度增加一維,數值為1,axis=-1表示最後一位
                                        #當然,我們常用tf.reshape(input, shape=[])可達到相同效果,但有時在構建圖的過程中,placeholder沒有被feed具體的值,就會報錯
                                        #'t' is a tensor of shape [2]
                                        # shape(expand_dims(t, 0)) ==> [1, 2]
                                        # shape(expand_dims(t, 1)) ==> [2, 1]
                                        # shape(expand_dims(t, -1)) ==> [2, 1]
    
    #對文字嵌入層使用不同尺寸的卷積核做卷積和最大池化
    pool_layer_lst = []
    for window_size in window_sizes:#[2,3,4,5]
        with tf.name_scope("movie_txt_conv_maxpool_{}".format(window_size)): #格式化字串的函式 str.format()
                                                                             #通過 {} 和 : 來代替以前的 % 
            filter_weights = tf.Variable(tf.truncated_normal([window_size, embed_dim, 1, filter_num],stddev=0.1),name = "filter_weights")
                            #初始化卷積核引數
                            #tf.truncated_normal(shape, mean=0.0, stddev=1.0, dtype=tf.float32, seed=None, name=None)
                            #得到正態分佈輸出為shape,mean均值,stddev標準差
            filter_bias = tf.Variable(tf.constant(0.1, shape=[filter_num]), name="filter_bias")
                            #初始化bias
                            #tf.constant(value,dtype=None,shape=None,name=’Const’) 建立一個常量tensor,按照給出value來賦值,可以用shape來指定其形狀
                            #這裡的shape表示(8,)
            conv_layer = tf.nn.conv2d(movie_title_embed_layer_expand, filter_weights, [1,1,1,1], padding="VALID", name="conv_layer")
                        #tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, name=None)
                        #input:指需要做卷積的輸入影象 filter:相當於CNN中的卷積核 strides:卷積時在影象每一維的步長通常為[1,X,X,1]
                        #padding:string型別的量,只能是"SAME","VALID"其中之一,這裡不填充
                        #use_cudnn_on_gpu:bool型別,是否使用cudnn加速,預設為true
            relu_layer = tf.nn.relu(tf.nn.bias_add(conv_layer,filter_bias), name ="relu_layer") #最後迭代到windowsize=5時(?, 11, 1, 8)
            
            maxpool_layer = tf.nn.max_pool(relu_layer, [1,sentences_size - window_size + 1 ,1,1], [1,1,1,1], padding="VALID", name="maxpool_layer")
                        #(?, 1, 1, 8)
                        #tf.nn.max_pool(value, ksize, strides, padding, name=None)
                        #value:需要池化的輸入 
                        #ksize:池化視窗的大小,取一個四維向量,一般是[1, height, width, 1],因為我們不想在batch和channels上做池化,所以這兩個維度設為了1
                        #height=15-windowsize
                        #strides:和卷積類似,視窗在每一個維度上滑動的步長,一般也是[1, stride,stride, 1]
            pool_layer_lst.append(maxpool_layer)
            #這裡最後得到4個tensor堆疊的列表
            #[<tf.Tensor 'movie_txt_conv_maxpool_2/maxpool_layer:0' shape=(?, 1, 1, 8) dtype=float32>, 
            #<tf.Tensor 'movie_txt_conv_maxpool_3/maxpool_layer:0' shape=(?, 1, 1, 8) dtype=float32>, 
            #<tf.Tensor 'movie_txt_conv_maxpool_4/maxpool_layer:0' shape=(?, 1, 1, 8) dtype=float32>, 
            #<tf.Tensor 'movie_txt_conv_maxpool_5/maxpool_layer:0' shape=(?, 1, 1, 8) dtype=float32>]


    #Dropout層
    with tf.name_scope("pool_dropout"):
        pool_layer = tf.concat(pool_layer_lst, 3, name ="pool_layer") #(?, 1, 1, 32)
        max_num = len(window_sizes) * filter_num #32
        pool_layer_flat = tf.reshape(pool_layer , [-1, 1, max_num], name = "pool_layer_flat") #(?, 1, 32) 
    
        dropout_layer = tf.nn.dropout(pool_layer_flat, dropout_keep_prob, name = "dropout_layer") #(?, 1, 32)
                       #tf.nn.dropout(x, keep_prob, noise_shape=None, seed=None, name=None)
                       #x:輸入;keep_prob:保留比例,取值 (0,1],每一個引數都將按這個比例隨機變更
                       #dropout是CNN中防止過擬合的一個trick
                       #dropout是指在深度學習網路的訓練過程中,對於神經網路單元,按照一定的概率將其暫時從網路中丟棄。
                       #注意是暫時,對於隨機梯度下降來說,由於是隨機丟棄,故而每一個mini-batch都在訓練不同的網路。
    return pool_layer_flat, dropout_layer
    
#將Movie的各個層一起做全連線
def get_movie_feature_layer(movie_id_embed_layer, movie_categories_embed_layer, dropout_layer):
    with tf.name_scope("movie_fc"):
        #第一層全連線
        movie_id_fc_layer = tf.layers.dense(movie_id_embed_layer, embed_dim, name = "movie_id_fc_layer", activation=tf.nn.relu)
        movie_categories_fc_layer = tf.layers.dense(movie_categories_embed_layer, embed_dim, name = "movie_categories_fc_layer", activation=tf.nn.relu)
    
        #第二層全連線
        movie_combine_layer = tf.concat([movie_id_fc_layer, movie_categories_fc_layer, dropout_layer], 2)  #(?, 1, 96)
        movie_combine_layer = tf.contrib.layers.fully_connected(movie_combine_layer, 200, tf.tanh)  #(?, 1, 200)
    
        movie_combine_layer_flat = tf.reshape(movie_combine_layer, [-1, 200])#(?,200)
    return movie_combine_layer, movie_combine_layer_flat

#構建計算圖-----------------------------------------

#變數常量等等基本量的操作設定完成,意味著最基本的東西都有了,然後接下來很重要的就是那些量和操作怎麼組成更大的集合,怎麼執行這個集合
#這些就是計算圖譜Graph和Session的作用:(參見https://blog.csdn.net/xierhacker/article/details/53860379)
#一、graph
    #一個TensorFlow的運算,被表示為一個數據流的圖。 
    #一幅圖中包含一些操作(Operation)物件,這些物件是計算節點。前面說過的Tensor物件,則是表示在不同的操作(operation)間的資料節點
    #一旦開始任務,就已經有一個預設的圖建立好了。可以通過呼叫tf.get_default_graph()來訪問。新增一個操作到預設的圖裡面,只需呼叫一個定義了新操作的函式
    #另外一種典型用法是要使用到Graph.as_default()的上下文管理器(context manager),它能夠在這個上下文裡面覆蓋預設的圖.要在某個graph裡面定義量,要在with語句的範圍裡面定義
#二.Session(tf.Session)
    #執行TensorFLow操作(operations)的類,一個Seesion包含了操作物件執行的環境
#

#tensorflow中的計算以圖資料流的方式表示,一個圖包含一系列表示計算單元的操作物件,以及在圖中流動的資料單元以tensor物件
tf.reset_default_graph()#用於清除預設圖形堆疊並重置全域性預設圖形
train_graph = tf.Graph()# first creat a simple graph

with train_graph.as_default(): #define a simple graph
                               #返回一個上下文管理器,使得這個Graph物件成為當前預設的graph.當你想在一個程序裡面建立多個圖的時候,就應該使用這個函式.
                               #為了方便,一個全域性的圖物件被預設提供,要是沒有顯式建立一個新的圖的話,所有的操作(ops)都會被新增到這個預設的圖裡面來. 
                               #通過with關鍵字和這個方法,來讓這個程式碼塊內建立的從操作(ops)新增到這個新的圖裡面. 
                               
                               #with Object() as obj:的時候,自動呼叫obj物件的__enter__()方法,而當出去with block的時候,又會呼叫obj物件的__exit__方法。
                               #正是利用 __enter__()和__exit__(),才實現類似上下文管理器的作用。
                               #參見https://blog.csdn.net/u012436149/article/details/73555017
    #獲取輸入佔位符
    uid, user_gender, user_age, user_job, movie_id, movie_categories, movie_titles, targets, lr, dropout_keep_prob = get_inputs()
    #獲取User的4個嵌入向量
    uid_embed_layer, gender_embed_layer, age_embed_layer, job_embed_layer = get_user_embedding(uid, user_gender, user_age, user_job)
    #得到使用者特徵
    user_combine_layer, user_combine_layer_flat = get_user_feature_layer(uid_embed_layer, gender_embed_layer, age_embed_layer, job_embed_layer)
    #獲取電影ID的嵌入向量
    movie_id_embed_layer = get_movie_id_embed_layer(movie_id)
    #獲取電影型別的嵌入向量
    movie_categories_embed_layer = get_movie_categories_layers(movie_categories)
    #獲取電影名的特徵向量
    pool_layer_flat, dropout_layer = get_movie_cnn_layer(movie_titles)
    #得到電影特徵
    movie_combine_layer, movie_combine_layer_flat = get_movie_feature_layer(movie_id_embed_layer, 
                                                                                movie_categories_embed_layer, 
                                                                                dropout_layer)
    #計算出評分,要注意兩個不同的方案,inference的名字(name值)是不一樣的,後面做推薦時要根據name取得tensor
    with tf.name_scope("inference"):
        #將使用者特徵和電影特徵作為輸入,經過全連線,輸出一個值的方案
#         inference_layer = tf.concat([user_combine_layer_flat, movie_combine_layer_flat], 1)  #(?, 200)
#         inference = tf.layers.dense(inference_layer, 1,
#                                     kernel_initializer=tf.truncated_normal_initializer(stddev=0.01), 
#                                     kernel_regularizer=tf.nn.l2_loss, name="inference")

        #簡單的將使用者特徵和電影特徵做矩陣乘法得到一個預測評分
#        inference = tf.matmul(user_combine_layer_flat, tf.transpose(movie_combine_layer_flat)) 錯誤應該是下面一個使用者與一個電影的評分 
        inference = tf.reduce_sum(user_combine_layer_flat * movie_combine_layer_flat, axis=1)#按axis=1求和降維,得(?,),*表示對應元素相乘
        inference = tf.expand_dims(inference, axis=1) #(batch,1)為了下面和target統一格式計算loss

    with tf.name_scope("loss"):
        #將梯度在target network和learner間傳遞的功能在distributed tensorflow中預設已經實現好了
        #Between-graph的方式中,每個thread會拷貝一份Graph,計算後回傳回主Graph。需要解決的主要是梯度累積的問題。
        # MSE損失,將計算值迴歸到評分
        cost = tf.losses.mean_squared_error(targets, inference )
        loss = tf.reduce_mean(cost) #tf.reduce_mean 函式用於計算張量tensor沿著指定的數軸(tensor的某一維度)上的的平均值,主要用作降維或者計算tensor(影象)的平均值。

        # 優化損失 
#     train_op = tf.train.AdamOptimizer(lr).minimize(loss)  #cost
    global_step = tf.Variable(0, name="global_step", trainable=False)
    optimizer = tf.train.AdamOptimizer(lr)#進行反向傳播的訓練方法
    gradients = optimizer.compute_gradients(loss)  #cost
                #minimize() = compute_gradients() + apply_gradients()拆分成計算梯度和應用梯度兩個步驟
                #tf.train.Optimizer.compute_gradients(loss, var_list=None, gate_gradients=1) 計算“var_list”中變數的“loss”梯度
                #返回一個元素為(loss對引數梯度,這個引數引數)對的列表[ (), () ],其中每個tuple對中梯度是該引數的梯度值
                #loss:包含要最小化的值的Tensor
    train_op = optimizer.apply_gradients(gradients, global_step=global_step)
                #tf.train.Optimizer.apply_gradients(grads_and_vars, global_step=None, name=None)進行更新網路,即應用梯度
                #minimize() 的第二部分,將上一步得到的字典,進行更新引數
                #grads_and_vars: 上一步得到的[(gradient, variable)] 
#取得batch------------------------------------------
def get_batches(Xs, ys, batch_size):
    for start in range(0, len(Xs), batch_size):
        end = min(start + batch_size, len(Xs))
        yield Xs[start:end], ys[start:end] #yield 是一個類似 return 的關鍵字,迭代一次遇到yield時就返回yield後面(右邊)的值。
                                           #重點是:下一次迭代時,從上一次迭代遇到的yield後面的程式碼(下一行)開始執行。
                                           #參見https://www.jianshu.com/p/d09778f4e055
        
#訓練網路-------------------------------------------
%matplotlib inline
                    #ipython notebook中有一個相當方便的語句: %matplotlib inline,可以實現執行cell即出現結果影象。
                    #但是如果想寫在Python程式內,顯示影象,需要加上一句: plt.show()。
                    #當然確保匯入了對應的庫: import matplotlib.pyplot as plt
                    
                    #魔法函式(Magic Functions)分兩種:一種是面向行的,另一種是面向單元型的。
                    #行magic函式:用字首“%”標註的,很像使用命令列時的形式,“%”後面就是magic函式的引數了,但引數是沒有被寫在括號或者引號中來傳值的
                    #單元型magic函式是由兩個“%%”做字首的,它的引數不僅是當前“%%”行後面的內容,也包括了在當前行以下的行
%config InlineBackend.figure_format = 'retina'
                    #為了使畫出來的圖支援 retina
                    #在解析度較高的螢幕(例如 Retina 顯示屏)上,notebook 中的預設影象可能會顯得模糊。
                    #可以在 %matplotlib inline 之後使用 %config InlineBackend.figure_format = 'retina' 來呈現解析度較高的影象。


losses = {'train':[], 'test':[]}

with tf.Session(graph=train_graph) as sess:
    
    #蒐集資料給tensorBoard用
    #跟蹤梯度值和稀疏度
    grad_summaries = []
    for g, v in gradients: #v對應的gradients
        if g is not None:
            grad_hist_summary = tf.summary.histogram("{}/grad/hist".format(v.name.replace(':', '_')), g)
                                #tf.summary.histogram('summary_name', tensor)用來顯示直方圖資訊
                                #將【計算圖】中的【資料的分佈/資料直方圖】寫入TensorFlow中的【日誌檔案】,以便為將來tensorboard的視覺化做準備
                                #一般用來顯示訓練過程中變數的分佈情況
            sparsity_summary = tf.summary.scalar("{}/grad/sparsity".format(v.name.replace(':', '_')), tf.nn.zero_fraction(g))
                                #tf.summary.scalar(name, tensor, collections=None) 用來顯示標量資訊,一般在畫loss,accuary時會用到這個函式
                                #將【計算圖】中的【標量資料】寫入TensorFlow中的【日誌檔案】,以便為將來tensorboard的視覺化做準備
                                #tf.nn.zero_fraction統計某個值的0的比例,這個tf.nn.zero_fraction計算出來的值越大,0的比例越高,稀疏度
            grad_summaries.append(grad_hist_summary)
            grad_summaries.append(sparsity_summary)
    grad_summaries_merged = tf.summary.merge(grad_summaries)
                                #tf.summary.merge(inputs, collections=None, name=None)將上面幾種型別的彙總再進行一次合併,具體合併哪些由inputs指定
                                # merge_all將之前定義的所有summary整合在一起
                                #[2]和TensorFlow中的其他操作類似,tf.summary.scalar、tf.summary.histogram、tf.summary.image函式也是一個op
                                #在定義的時候,也不會立即執行,需要通過sess.run來明確呼叫這些函式。因為,在一個程式中定義的寫日誌操作比較多
                                #如果一一呼叫,將會十分麻煩,所以Tensorflow提供了tf.summary.merge_all()函式將所有的summary整理在一起
                                #在TensorFlow程式執行的時候,只需要執行這一個操作就可以將程式碼中定義的所有【寫日誌操作】執行一次,從而將所有的日誌寫入【日誌檔案】。
        
        
        
    #模型和summaries的輸出目錄
    timestamp = str(int(time.time()))
    out_dir = os.path.abspath(os.path.join(os.path.curdir, "runs", timestamp))
            #os.path.curdir:當前目錄
            #os.path.join(): 常用來連結路徑
            #os.path.abspath 絕對路徑
                                    #Python中有join()和os.path.join()兩個函式
                                    #join(): 連線字串陣列。將字串、元組、列表中的元素以指定的字元(分隔符)連線生成一個新的字串 
                                    #os.path.join(): 將多個路徑組合後返回:os.path.join(path1[,path2[,……]])
                                        #第一個以”/”開頭的引數開始拼接,之前的引數全部丟棄
                                        #以上一種情況為先。在上一種情況確保情況下,若出現”./”開頭的引數,會從”./”開頭的引數的上一個引數開始拼接
                                        #
                                        #os.path.join('aaaa','/bbbb','ccccc.txt')--> /bbbb\ccccc.txt只有一個以”/”開頭的,引數從它開始往後拼接,之前的引數全部丟棄
                                        #os.path.join('/aaaa','/bbbb','/ccccc.txt')-->/ccccc.txt有多個以”/”開頭的引數,從最後”/”開頭的的開始往後拼接,之前的引數全部丟棄
                                        #os.path.join('aaaa','./bbb','ccccc.txt')-->aaaa\./bbb\ccccc.txt若出現”./”開頭的引數,會從”./”開頭的引數的上一個引數開始拼接,即只包含上一個引數加往後的
    print("Writing to {}\n".format(out_dir))
     
    #關於損失和準確性的summaries,顯示loss標量資訊
    loss_summary = tf.summary.scalar("loss", loss)

    #訓練 Summaries
    train_summary_op = tf.summary.merge([loss_summary, grad_summaries_merged])
    train_summary_dir = os.path.join(out_dir, "summaries", "train")
    train_summary_writer = tf.summary.FileWriter(train_summary_dir, sess.graph)
                           #FileWriter類提供了一種用於在給定目錄下建立事件檔案的機制,並且將summary資料寫入硬碟
                           #sess.graph這個引數表示將前面定義的框架資訊收集起來,放在train_summary_dir目錄下

    # Inference summaries
    inference_summary_op = tf.summary.merge([loss_summary])
    inference_summary_dir = os.path.join(out_dir, "summaries", "inference")
    inference_summary_writer = tf.summary.FileWriter(inference_summary_dir, sess.graph)
                               #tf.summary.FileWritter(path,sess.graph)指定一個檔案用來儲存圖
                               #可以呼叫其add_summary()方法將訓練過程資料儲存在filewriter指定的檔案中

    sess.run(tf.global_variables_initializer()) #引數的初始化
    
    
    saver = tf.train.Saver() #用於後面儲存資料,建立一個saver物件
    for epoch_i in range(num_epochs):
        
        #將資料集分成訓練集和測試集,隨機種子不固定
        train_X,test_X, train_y, test_y = train_test_split(features,  
                                                           targets_values,  
                                                           test_size = 0.2,  
                                                           random_state = 0)  
        
        train_batches = get_batches(train_X, train_y, batch_size)#在分好的訓練集中再選batch個
        test_batches = get_batches(test_X, test_y, batch_size)
    
        #訓練的迭代,儲存訓練損失
        for batch_i in range(len(train_X) // batch_size): #//取結果的最小整數
            x, y = next(train_batches) #next() 返回迭代器的下一個專案,next(get_batches(train_X, train_y, batch_size))
                                       #在這個for迴圈每次都進行上一個batch的後面一個batch

            categories = np.zeros([batch_size, 18])
            for i in range(batch_size):
                categories[i] = x.take(6,1)[i] #x取縱著的下標為6的全部資料
                
            titles = np.zeros([batch_size, sentences_size])
            for i in range(batch_size):
                titles[i] = x.take(5,1)[i]

            feed = {
                uid: np.reshape(x.take(0,1), [batch_size, 1]),
                user_gender: np.reshape(x.take(2,1), [batch_size, 1]),
                user_age: np.reshape(x.take(3,1), [batch_size, 1]),
                user_job: np.reshape(x.take(4,1), [batch_size, 1]),
                movie_id: np.reshape(x.take(1,1), [batch_size, 1]),
                movie_categories: categories,  #x.take(6,1)
                movie_titles: titles,  #x.take(5,1)
                targets: np.reshape(y, [batch_size, 1]),
                dropout_keep_prob: dropout_keep, #dropout_keep
                lr: learning_rate}

            step, train_loss, summaries, _ = sess.run([global_step, loss, train_summary_op, train_op], feed)  #cost
            losses['train'].append(train_loss)
            train_summary_writer.add_summary(summaries, step)  #
            
            # Show every <show_every_n_batches> batches
            if (epoch_i * (len(train_X) // batch_size) + batch_i) % show_every_n_batches == 0:
                time_str = datetime.datetime.now().isoformat()
                print('{}: Epoch {:>3} Batch {:>4}/{}   train_loss = {:.3f}'.format(
                    time_str,
                    epoch_i,
                    batch_i,
                    (len(train_X) // batch_size),
                    train_loss))
                
        #使用測試資料的迭代
        for batch_i  in range(len(test_X) // batch_size):
            x, y = next(test_batches)
            
            categories = np.zeros([batch_size, 18])
            for i in range(batch_size):
                categories[i] = x.take(6,1)[i]

            titles = np.zeros([batch_size, sentences_size])
            for i in range(batch_size):
                titles[i] = x.take(5,1)[i]

            feed = {
                uid: np.reshape(x.take(0,1), [batch_size, 1]),
                user_gender: np.reshape(x.take(2,1), [batch_size, 1]),
                user_age: np.reshape(x.take(3,1), [batch_size, 1]),
                user_job: np.reshape(x.take(4,1), [batch_size, 1]),
                movie_id: np.reshape(x.take(1,1), [batch_size, 1]),
                movie_categories: categories,  #x.take(6,1)
                movie_titles: titles,  #x.take(5,1)
                targets: np.reshape(y, [batch_size, 1]),
                dropout_keep_prob: 1,
                lr: learning_rate}
            
            step, test_loss, summaries = sess.run([global_step, loss, inference_summary_op], feed)  #cost

            #儲存測試損失
            losses['test'].append(test_loss)
            inference_summary_writer.add_summary(summaries, step)  #呼叫add_summary()方法將訓練過程資料儲存在filewriter指定的檔案中

            time_str = datetime.datetime.now().isoformat() #datetime.datetime:表示日期(year, month, day)時間(hour, minute, second, microsecond)
                                                           #date.isoformat():返回格式如'YYYY-MM-DD’的字串
            if (epoch_i * (len(test_X) // batch_size) + batch_i) % show_every_n_batches == 0:
                print('{}: Epoch {:>3} Batch {:>4}/{}   test_loss = {:.3f}'.format(
                    time_str,
                    epoch_i,
                    batch_i,
                    (len(test_X) // batch_size),
                    test_loss))

    # Save Model儲存模型
    saver.save(sess, save_dir)  #sess是一個session物件,這裡save_dir是給模型起的名字
                                #可以加引數 global_step=epoch_i,表示進行epoch_i次儲存一次模型
    print('Model Trained and Saved')

save_params((save_dir)) #儲存引數 儲存save_dir 在生成預測時使用

#顯示訓練Loss
plt.plot(losses['train'], label='Training loss')#plt.plot(x,y)
plt.legend() #用於顯示圖例
_ = plt.ylim() #設定y軸刻度的取值範圍,不指定則自適應橫縱座標軸

#顯示測試loss
plt.plot(losses['test'], label='Test loss')
plt.legend()
_ = plt.ylim()



模型結構如圖(我自己寫的)