Beacon学习周记4(2022暑期项目)

2022.9.2

学习总结

本周看完了Beacon的代码,开始了第一次训练。

Beacon源码学习总结

上周未学完的models.py:

            with tf.name_scope("Loss"):
                self.loss = self.compute_loss(logits, self.y)

                self.predictions = tf.nn.sigmoid(logits)
                self.top_k_values, self.top_k_indices = tf.nn.top_k(self.predictions, 200)  # 找出分最高的200个产品的分数和序号
                self.recall_at_k = self.compute_recall_at_topk(top_k)  # 返回预测准确率

            # Adam optimizer
            train_op = tf.train.RMSPropOptimizer(learning_rate=self.learning_rate)
            # Op to calculate every variable gradient
            self.grads = train_op.compute_gradients(self.loss, tf.trainable_variables())
            self.update_grads = train_op.apply_gradients(self.grads)

            # Summarize all variables and their gradients
            total_parameters = 0
            print("-------------------- SUMMARY ----------------------")
            tf.summary.scalar("C_Basket", self.C_Basket)

            for grad, var in self.grads:
                tf.summary.histogram(var.name, var)
                tf.summary.histogram(var.name + '/grad', grad)

                shape = var.get_shape()
                variable_parameters = 1
                for dim in shape:
                    variable_parameters *= dim.value
                print("+ {:<64} {:<10,} parameter(s)".format(var.name, variable_parameters))
                total_parameters += variable_parameters

            print("Total number of parameters: {:,}".format(total_parameters))
            print("----------------- END SUMMARY ----------------------\n")

            # Create a summary to monitor cost tensor
            tf.summary.scalar("Train_Batch_Loss", self.loss)
            #tf.summary.scalar("Train_Batch_Recall", self.recall_at_k)

            # Create a summary to monitor cost tensor
            tf.summary.scalar("Val_Batch_Loss", self.loss, collections=['validation'])
            #tf.summary.scalar("Val_Batch_Recall", self.recall_at_k, collections=['validation'])

            # Merge all summaries into a single op
            self.merged_summary_op = tf.summary.merge_all()
            self.val_merged_summary_op = tf.summary.merge_all(key='validation')

    def train_batch(self, s, s_length, y):  # 训练批
        bseq_indices, bseq_values = self.get_sparse_tensor_info(s, True)

        _, loss, recall, summary = self.session.run(
            [self.update_grads, self.loss, self.recall_at_k, self.merged_summary_op],
            feed_dict={self.bseq_length: s_length, self.y: y,
                       self.bseq.indices: bseq_indices, self.bseq.values: bseq_values})

        return loss, recall, summary

    def validate_batch(self, s, s_length, y):  # 验证批
        bseq_indices, bseq_values = self.get_sparse_tensor_info(s, True)

        loss, recall, summary = self.session.run(
            [self.loss, self.recall_at_k, self.val_merged_summary_op],
            feed_dict={ self.bseq_length: s_length, self.y: y,
                        self.bseq.indices: bseq_indices, self.bseq.values: bseq_values})
        return loss, recall, summary

    def generate_prediction(self, s, s_length):
        bseq_indices, bseq_values = self.get_sparse_tensor_info(s, True)
        return self.session.run([self.top_k_values, self.top_k_indices],
                                 feed_dict={self.bseq_length: s_length, self.bseq.indices: bseq_indices, self.bseq.values: bseq_values})

    def encode_basket_graph(self, binput, beta, is_sparse=False):
        with tf.name_scope("Graph_Encoder"):
            if is_sparse:
                encoder = tf.sparse_tensor_dense_matmul(binput, self.I_B_Diag, name="XxI_B")  # 返回binput和I_B_Diag的乘积XxI_B
                encoder += self.relu_with_threshold(tf.sparse_tensor_dense_matmul(binput, self.A, name="XxA"), beta)  # 返回ReLu(XxA-β) 
            else:
                encoder = tf.matmul(binput, self.I_B_Diag, name="XxI_B")  # 若不是稀疏矩阵就不用sparse_tensor_dense_matmul方法了
                encoder += self.relu_with_threshold(tf.matmul(binput, self.A, name="XxA"), beta)  # 返回ReLu(XxA-β) 
        return encoder  # 此时encoder=XxI_B+ReLu(XxA-β)

    def get_item_bias(self):
        return self.session.run(self.I_B)  # 取回item bias

    def get_sparse_tensor_info(self, x, is_bseq=False):
        indices = []
        if is_bseq:
            for sid, bseq in enumerate(x):
                for t, basket in enumerate(bseq):
                    for item_id in basket:
                        indices.append([sid, t, item_id])  # 让indices成为一个存储序列号(从0开始)、序列中的篮子号(从0开始)、和物品ID的列表
        else:
            for bid, basket in enumerate(x):
                for item_id in basket:
                    indices.append([bid, item_id])  # 让indices成为一个存储篮子号(从0开始)、和物品ID的列表

        values = np.ones(len(indices), dtype=np.float32)  # 创建一个元素个数为总物品个数的全1数组
        indices = np.array(indices, dtype=np.int32)

        return indices, values  # 返回indices数组和全1数组

    def compute_loss(self, logits, y):
        sigmoid_logits = tf.nn.sigmoid(logits)

        neg_y = (1.0 - y)
        pos_logits = y * logits

        pos_max = tf.reduce_max(pos_logits, axis=1)  # 把pos_logits每一行中最大的元素提取出来(如果是二维张量的话。因为这里以第二维——行为轴)
        pos_max = tf.expand_dims(pos_max, axis=-1)  # 把这些最大元素的最高维(也就是最内层括号内的元素们)再每个套个括号(相当于增加一维)

        pos_min = tf.reduce_min(pos_logits + neg_y * pos_max, axis=1)  # 把pos_logits + neg_y * pos_max每一行中最小的元素提取出来
        pos_min = tf.expand_dims(pos_min, axis=-1)  # 把这些最小元素的最高维(也就是最内层括号内的元素们)再每个套个括号(相当于增加一维)

        nb_pos, nb_neg = tf.count_nonzero(y, axis=1), tf.count_nonzero(neg_y, axis=1)  # 把每一行的非零元素个数做成数组
        ratio = tf.cast(nb_neg, dtype=tf.float32) / tf.cast(nb_pos, dtype=tf.float32)  # 然后把这两个非零元素个数相除得到ratio

        pos_weight = tf.expand_dims(ratio, axis=-1)
        loss = y * -tf.log(sigmoid_logits) * pos_weight + neg_y * -tf.log(1.0 - tf.nn.sigmoid(logits - pos_min))  # 论文中的式8

        return tf.reduce_mean(loss + 1e-8)  # 算出损失均值
    
    def compute_recall_at_topk(self, k=10):  # 推荐K个物品作为下一个篮子,默认10个
        top_k_preds = self.get_topk_tensor(self.predictions, k)
        correct_preds = tf.count_nonzero(tf.multiply(self.y, top_k_preds), axis=1)  # top_k_preds元素非1即0。做element-wise相乘后,可以提取出y中那些评分最高的元素。再对提取出来的矩阵统计每一行的非零元素个数,理论上不是1就是0
        actual_bsize = tf.count_nonzero(self.y, axis=1)  # 实际上的非零元素个数
        return tf.reduce_mean(tf.cast(correct_preds, dtype=tf.float32) / tf.cast(actual_bsize, dtype=tf.float32))  # 两者相比取均值,就是预测准确率
    
    # x -> shape(batch_size,N)
    def get_topk_tensor(self, x, k=10):
        _, index_cols = tf.nn.top_k(x, k)  # 找出最大的10个

        index_rows = tf.ones(shape=(self.batch_size, k), dtype=tf.int32) * tf.expand_dims(tf.range(0, self.batch_size), axis=-1)  # 行序号

        index_rows = tf.cast(tf.reshape(index_rows, shape=[-1]), dtype=tf.int64)
        index_cols = tf.cast(tf.reshape(index_cols, shape=[-1]), dtype=tf.int64)

        top_k_indices = tf.stack([index_rows, index_cols], axis=1)  # 现在top_k_indices应该是2列,第一列是range(0, self.batch_size),第二列是最大的10个的原序号
        top_k_values = tf.ones(shape=[self.batch_size * k], dtype=tf.float32)

        sparse_tensor = tf.SparseTensor(indices=top_k_indices, values=top_k_values, dense_shape=[self.batch_size, self.nb_items])  # 现在sparse_tensor是一个batch_sizeXnb_items形状的矩阵,它的每一行都会有一个元素为1,其位置代表着原来的序号,相当于其每一行都是one-hot的
        return tf.sparse_tensor_to_dense(tf.sparse_reorder(sparse_tensor))  # 返回之
    
    def relu_with_threshold(self, x, threshold):
        return tf.nn.relu(x - tf.abs(threshold))
    

procedure.py:

import tensorflow as tf
import numpy as np

import sys
import utils
import time


def train_network(sess, net, train_generator, validate_generator, nb_epoch, 
                  total_train_batches, total_validate_batches, display_step,
                  early_stopping_k, epsilon, tensorboard_dir, output_dir,
                  test_generator, total_test_batches):
    summary_writer = None
    if tensorboard_dir is not None:
        summary_writer = tf.summary.FileWriter(tensorboard_dir)
    # Add ops to save and restore all the variables.
    saver = tf.train.Saver()

    val_best_performance = [sys.float_info.max]  # 最大可表示的正有限浮点数
    patience_cnt = 0
    for epoch in range(0, nb_epoch):  # 开始走epoch
        print("\n=========================================")
        print("@Epoch#" + str(epoch))

        train_loss = 0.0
        train_recall = 0.0

        for batch_id, data in train_generator:  # 先训练
            start_time = time.time()
            loss, recall, summary = net.train_batch(data['S'], data['L'], data['Y'])  # S是篮子序列们,L是篮子序列长度们,Y是最后一个篮子的二进制表示们

            train_loss += loss
            avg_train_loss = train_loss / (batch_id + 1)  # 当前的平均训练损失

            train_recall += recall
            avg_train_recall = train_recall / (batch_id + 1)  # 当前的平均训练召回率(召回率是正例有多少被预测正确了(找得全);精确率是预测为正的样本中有多少是真正的正样本(找得对))
            
            # Write logs at every iteration
            if summary_writer is not None:
                summary_writer.add_summary(summary, epoch * total_train_batches + batch_id)

                loss_sum = tf.Summary()
                loss_sum.value.add(tag="Losses/Train_Loss", simple_value=avg_train_loss)
                summary_writer.add_summary(loss_sum, epoch * total_train_batches + batch_id)

                recall_sum = tf.Summary()
                recall_sum.value.add(tag="Recalls/Train_Recall", simple_value=avg_train_recall)
                summary_writer.add_summary(recall_sum, epoch * total_train_batches + batch_id)


            if batch_id % display_step == 0 or batch_id == total_train_batches - 1:  # 训练全部批完了后,统计各种指标
                running_time = time.time() - start_time
                print("Training | Epoch " + str(epoch) + " | " + str(batch_id + 1) + "/" + str(total_train_batches) 
                    + " | Loss= " + "{:.8f}".format(avg_train_loss)  
                    + " | Recall@"+ str(net.top_k) + " = " + "{:.8f}".format(avg_train_recall) 
                    + " | Time={:.2f}".format(running_time) + "s")

            if batch_id >= total_train_batches - 1:
                break

        print("\n-------------- VALIDATION LOSS--------------------------")  # 再验证
        val_loss = 0.0
        val_recall = 0.0
        for batch_id, data in validate_generator:
            loss, recall, summary = net.validate_batch(data['S'], data['L'], data['Y'])
            
            val_loss += loss
            avg_val_loss = val_loss / (batch_id + 1)

            val_recall += recall
            avg_val_recall = val_recall / (batch_id + 1)

            # Write logs at every iteration
            if summary_writer is not None:
                summary_writer.add_summary(summary, epoch * total_validate_batches + batch_id)

                loss_sum = tf.Summary()
                loss_sum.value.add(tag="Losses/Val_Loss", simple_value=avg_val_loss)
                summary_writer.add_summary(loss_sum, epoch * total_validate_batches + batch_id)

                recall_sum = tf.Summary()
                recall_sum.value.add(tag="Recalls/Val_Recall", simple_value=avg_val_recall)
                summary_writer.add_summary(recall_sum, epoch * total_validate_batches + batch_id)

            if batch_id % display_step == 0 or batch_id == total_validate_batches - 1:
                print("Validating | Epoch " + str(epoch) + " | " + str(batch_id + 1) + "/" + str(total_validate_batches) 
                    + " | Loss = " + "{:.8f}".format(avg_val_loss)
                    + " | Recall@"+ str(net.top_k) + " = " + "{:.8f}".format(avg_val_recall))
            
            if batch_id >= total_validate_batches - 1:
                break

        print("\n-------------- TEST LOSS--------------------------")  # 然后后测试
        test_loss = 0.0
        test_recall = 0.0
        for batch_id, data in test_generator:
            loss, recall, _ = net.validate_batch(data['S'], data['L'], data['Y'])
            
            test_loss += loss
            avg_test_loss = test_loss / (batch_id + 1)

            test_recall += recall
            avg_test_recall = test_recall / (batch_id + 1)

            # Write logs at every iteration
            if summary_writer is not None:
                #summary_writer.add_summary(summary, epoch * total_test_batches + batch_id)

                loss_sum = tf.Summary()
                loss_sum.value.add(tag="Losses/Test_Loss", simple_value=avg_test_loss)
                summary_writer.add_summary(loss_sum, epoch * total_test_batches + batch_id)

                recall_sum = tf.Summary()
                recall_sum.value.add(tag="Recalls/Test_Recall", simple_value=avg_test_recall)
                summary_writer.add_summary(recall_sum, epoch * total_test_batches + batch_id)

            if batch_id % display_step == 0 or batch_id == total_test_batches - 1:
                print("Testing | Epoch " + str(epoch) + " | " + str(batch_id + 1) + "/" + str(total_test_batches) 
                    + " | Loss = " + "{:.8f}".format(avg_test_loss)
                    + " | Recall@"+ str(net.top_k) + " = " + "{:.8f}".format(avg_test_recall))
            
            if batch_id >= total_test_batches - 1:
                break

        if summary_writer is not None:
            I_B= net.get_item_bias()
            item_probs = net.item_probs

            I_B_corr = np.corrcoef(I_B, item_probs)
            I_B_summ = tf.Summary()
            I_B_summ.value.add(tag="CorrCoef/Item_Bias", simple_value=I_B_corr[1][0])
            summary_writer.add_summary(I_B_summ, epoch)

        avg_val_loss = val_loss / total_validate_batches
        print("\n@ The validation's loss = " + str(avg_val_loss))
        imprv_ratio = (val_best_performance[-1] - avg_val_loss)/val_best_performance[-1]
        if imprv_ratio > epsilon:
            print("# The validation's loss is improved from " + "{:.8f}".format(val_best_performance[-1]) + \
                  " to " + "{:.8f}".format(avg_val_loss))
            val_best_performance.append(avg_val_loss)  # 不断更新最好表现

            patience_cnt = 0

            save_dir = output_dir + "/epoch_" + str(epoch)
            utils.create_folder(save_dir)

            save_path = saver.save(sess, save_dir + "/model.ckpt")
            print("The model is saved in: %s" % save_path)
        else:
            patience_cnt += 1

        if patience_cnt >= early_stopping_k:
            print("# The training is early stopped at Epoch " + str(epoch))  # 如果害怕模型训练时间太长,那就设置一个patience_cnt作为训练最大次数。当达到这个最大次数了不管训没训练完会强制停止
            break

def tune(net, data_generator, total_batches, display_step, output_file):
    f = open(output_file, "w")
    val_loss = 0.0
    val_recall = 0.0
    for batch_id, data in data_generator:
        loss, recall, _ = net.validate_batch(data['S'], data['L'], data['Y'])
            
        val_loss += loss
        avg_val_loss = val_loss / (batch_id + 1)

        val_recall += recall
        avg_val_recall = val_recall / (batch_id + 1)

        # Write logs at every iteration
        if batch_id % display_step == 0 or batch_id == total_batches - 1:
            print(str(batch_id + 1) + "/" + str(total_batches) + " | Loss = " + "{:.8f}".format(avg_val_loss)
                    + " | Recall@"+ str(net.top_k) + " = " + "{:.8f}".format(avg_val_recall))

        if batch_id >= total_batches - 1:
            break
    avg_val_recall = val_recall / total_batches
    f.write(str(avg_val_recall) + "\n")  # 写入总的平均召回率
    f.close()


def generate_prediction(net, data_generator, total_test_batches, display_step, inv_item_dict, output_file):
    f = open(output_file, "w")

    for batch_id, data in data_generator:
        values, indices = net.generate_prediction(data['S'], data['L'])

        for i, (seq_val, seq_ind) in enumerate(zip(values, indices)):
            f.write("Target:" + data['O'][i])

            for (v, idx) in zip(seq_val, seq_ind):
                f.write("|" + str(inv_item_dict[idx]) + ":" + str(v))

            f.write("\n")

        if batch_id % display_step == 0 or batch_id == total_test_batches - 1:
            print(str(batch_id + 1) + "/" + str(total_test_batches))

        if batch_id >= total_test_batches - 1:
            break
    f.close()
    print(" ==> PREDICTION HAS BEEN DONE!")


def recent_model_dir(dir):
    folder_list = utils.list_directory(dir, True)
    folder_list = sorted(folder_list, key=get_epoch)
    return folder_list[-1]  # 返回最近一次训练的模型目录


def get_epoch(x):
    idx = x.index('_') + 1
    return int(x[idx:])

procedure.py中描述了网络模型的训练过程。

接下来是main_gpu.py:

import tensorflow as tf
import numpy as np
import scipy.sparse as sp
import os
import utils
import models
import procedure

# Parameters
# ###########################
# GPU & Seed

tf.flags.DEFINE_string("device_id", None, "GPU device is to be used in training (default: None)")
tf.flags.DEFINE_integer("seed", 2, "Seed value for reproducibility (default: 89)")  # 真的有可复现性吗?

# Model hyper-parameters
tf.flags.DEFINE_string("data_dir", None, "The input data directory (default: None)")
tf.flags.DEFINE_string("output_dir", None, "The output directory (default: None)")
tf.flags.DEFINE_string("tensorboard_dir", None, "The tensorboard directory (default: None)")

tf.flags.DEFINE_integer("emb_dim", 2, "The dimensionality of embedding (default: 2)")  # 做2维的嵌入层,映射成2维向量
tf.flags.DEFINE_integer("rnn_unit", 4, "The number of hidden units of RNN (default: 4)")  # 隐藏层单元数
tf.flags.DEFINE_integer("nb_hop", 1, "The number of neighbor hops  (default: 1)")
tf.flags.DEFINE_float("alpha", 0.5, "The reguralized hyper-parameter (default: 0.5)")

tf.flags.DEFINE_integer("matrix_type", 1, "The type of adjacency matrix (0=zero,1=real,default:1)")

# Training hyper-parameters
tf.flags.DEFINE_integer("nb_epoch", 15, "Number of epochs (default: 15)")
tf.flags.DEFINE_integer("early_stopping_k", 5, "Early stopping patience (default: 5)")
tf.flags.DEFINE_float("learning_rate", 0.001, "Learning rate (default: 0.001)")
tf.flags.DEFINE_float("epsilon", 1e-8, "The epsilon threshold in training (default: 1e-8)")
tf.flags.DEFINE_float("dropout_rate", 0.3, "Dropout keep probability for RNN (default: 0.3)")
tf.flags.DEFINE_integer("batch_size", 32, "Batch size (default: 32)")
tf.flags.DEFINE_integer("display_step", 10, "Show loss/acc for every display_step batches (default: 10)")
tf.flags.DEFINE_string("rnn_cell_type", "LSTM", " RNN Cell Type like LSTM, GRU, etc. (default: LSTM)")
tf.flags.DEFINE_integer("top_k", 10, "Top K Accuracy (default: 10)")
tf.flags.DEFINE_boolean("train_mode", False, "Turn on/off the training mode (default: False)")
tf.flags.DEFINE_boolean("tune_mode", False, "Turn on/off the tunning mode (default: False)")
tf.flags.DEFINE_boolean("prediction_mode", False, "Turn on/off the testing mode (default: False)")

config = tf.flags.FLAGS
print("---------------------------------------------------")
print("SeedVal = " + str(config.seed))
print("\nParameters: " + str(config.__len__()))  # 参数数量
for iterVal in config.__iter__():
    print(" + {}={}".format(iterVal, config.__getattr__(iterVal)))  # 列出参数序号和名称
print("Tensorflow version: ", tf.__version__)  # 打印TensorFlow版本
print("---------------------------------------------------")

os.environ["CUDA_DEVICE_ORDER"]="PCI_BUS_ID"  # 按照PCI_BUS_ID顺序从0开始排列GPU设备
os.environ["CUDA_VISIBLE_DEVICES"] = config.device_id  # 获取让Python检测到并使用的显卡

# for reproducibility
np.random.seed(config.seed)  # 设置好随机数种子
tf.set_random_seed(config.seed)  # Sets the graph-level random seed for the default graph.
tf.logging.set_verbosity(tf.logging.ERROR)  # Sets the threshold for what messages will be logged.

gpu_config = tf.ConfigProto()  # A ProtocolMessage
gpu_config.gpu_options.allow_growth = True  # 当allow_growth设置为True时,分配器将不会指定所有的GPU内存,而是根据需求增长allow_growth选项
gpu_config.log_device_placement = False  # 设置该参数log_device_placement=True,让我们可以看到我们的tensor、op是在哪台设备、哪颗CPU上运行的。

# ----------------------- MAIN PROGRAM -----------------------

data_dir = config.data_dir
output_dir = config.output_dir
tensorboard_dir=config.tensorboard_dir
# 三个数据集
training_file = data_dir + "/train.txt"
validate_file = data_dir + "/validate.txt"
testing_file = data_dir + "/test.txt"

print("***************************************************************************************")
print("Output Dir: " + output_dir)

# Create directories
print("@Create directories")
utils.create_folder(output_dir + "/models")  # 又创了两个目录
utils.create_folder(output_dir + "/topN")

if tensorboard_dir is not None:
    utils.create_folder(tensorboard_dir)  # 创tensorboard的目录

# Load train, validate & test
print("@Load train,validate&test data")
training_instances = utils.read_file_as_lines(training_file)  # 读取训练集
nb_train = len(training_instances)  # 序列数量
print(" + Total training sequences: ", nb_train)  # 打印序列数量

validate_instances = utils.read_file_as_lines(validate_file)  # 验证集,同上
nb_validate = len(validate_instances)
print(" + Total validating sequences: ", nb_validate)

testing_instances = utils.read_file_as_lines(testing_file)  # 测试集,同上
nb_test = len(testing_instances)
print(" + Total testing sequences: ", nb_test)

# Create dictionary
print("@Build knowledge")
MAX_SEQ_LENGTH, item_dict, rev_item_dict, item_probs = utils.build_knowledge(training_instances, validate_instances)

print("#Statistic")
NB_ITEMS = len(item_dict)  # 物品数量
print(" + Maximum sequence length: ", MAX_SEQ_LENGTH)
print(" + Total items: ", NB_ITEMS)

matrix_type = config.matrix_type
if matrix_type == 0:
    print("@Create an zero adjacency matrix")
    adj_matrix = utils.create_zero_matrix(NB_ITEMS)  # 如果指定创建一个类型为0的矩阵,就创建一个全0矩阵
else:
    print("@Load the normalized adjacency matrix")
    matrix_fpath = data_dir + "/adj_matrix/r_matrix_" + str(config.nb_hop)+ "w.npz"
    adj_matrix = sp.load_npz(matrix_fpath)
    print(" + Real adj_matrix has been loaded from" + matrix_fpath)  # 读取cmatrix_generator.py生成的邻接矩阵


print("@Compute #batches in train/validation/test")  # 返回并打印三种批的数量
total_train_batches = utils.compute_total_batches(nb_train, config.batch_size)
total_validate_batches = utils.compute_total_batches(nb_validate, config.batch_size)
total_test_batches = utils.compute_total_batches(nb_test, config.batch_size)
print(" + #batches in train ", total_train_batches)
print(" + #batches in validate ", total_validate_batches)
print(" + #batches in test ", total_test_batches)

model_dir = output_dir + "/models"
if config.train_mode:
    with tf.Session(config=gpu_config) as sess:
        # Training
        # ==================================================
        # Create data generator
        train_generator = utils.seq_batch_generator(training_instances, item_dict, config.batch_size)
        validate_generator = utils.seq_batch_generator(validate_instances, item_dict, config.batch_size, False)
        test_generator = utils.seq_batch_generator(testing_instances, item_dict, config.batch_size, False)
        
        # Initialize the network
        print(" + Initialize the network")
        net = models.Beacon(sess, config.emb_dim, config.rnn_unit, config.alpha, MAX_SEQ_LENGTH, item_probs, adj_matrix, config.top_k, 
                             config.batch_size, config.rnn_cell_type, config.dropout_rate, config.seed, config.learning_rate)

        print(" + Initialize parameters")
        sess.run(tf.global_variables_initializer())

        print("================== TRAINING ====================")
        print("@Start training")
        procedure.train_network(sess, net, train_generator, validate_generator, config.nb_epoch,
                                total_train_batches, total_validate_batches, config.display_step,
                                config.early_stopping_k, config.epsilon, tensorboard_dir, model_dir,
                                test_generator, total_test_batches)

        # Reset before re-load
    tf.reset_default_graph()

if config.prediction_mode or config.tune_mode:
    with tf.Session(config=gpu_config) as sess:
        print(" + Initialize the network")

        net = models.Beacon(sess, config.emb_dim, config.rnn_unit, config.alpha, MAX_SEQ_LENGTH, item_probs, adj_matrix, config.top_k, 
                        config.batch_size, config.rnn_cell_type, config.dropout_rate, config.seed, config.learning_rate)

        print(" + Initialize parameters")
        sess.run(tf.global_variables_initializer())

        print("===============================================\n")
        print("@Restore the model from " + model_dir)
        # Reload the best model
        saver = tf.train.Saver()
        recent_dir = utils.recent_model_dir(model_dir)
        saver.restore(sess, model_dir + "/" + recent_dir + "/model.ckpt")  # 把训练好的模型取回
        print("Model restored from file: %s" % recent_dir)

        # Tunning
        # ==================================================
        if config.tune_mode:
            print("@Start tunning")
            validate_generator = utils.seq_batch_generator(validate_instances, item_dict, config.batch_size, False)
            procedure.tune(net, validate_generator, total_validate_batches, config.display_step, output_dir + "/topN/val_recall.txt")  # 将召回率写入

        # Testing
        # ==================================================
        if config.prediction_mode:
            test_generator = utils.seq_batch_generator(testing_instances, item_dict, config.batch_size, False)

            print("@Start generating prediction")
            procedure.generate_prediction(net, test_generator, total_test_batches, config.display_step, 
                        rev_item_dict, output_dir + "/topN/prediction.txt")  # 将预测结果写入
        
    tf.reset_default_graph()

main_gpu.py是主程序。依靠着cmatrix_generator.py产生的邻接矩阵来训练模型。

训练总结

本周的训练碰到不少问题,并没有达到较好的训练效果。

现在总结本周的训练过程。

数据集

数据集来自Beacon代码的原作者提供的数据集。详见数据集

数据集示意

数据集如上图所示。可见确实是代码要求的格式(以”|“和” “分割)。

数据集每一行是一个篮子序列。并且首个”篮子“不是篮子而是类似标签的东西,取值0或1。这应该就是utils.py等文件中会不计算第一个”篮子“的原因。另外只有在validate.txt中该标签可能取值为1。

配环境

接下来必须得为Beacon专门配置一个Python环境。根据论文中的信息,所需环境如下:

  • Python == 3.6
  • Tensorflow == 1.14
  • scipy.sparse == 1.3.0

术业有专攻。由于电脑里已经安装了Anaconda 3,所以用Anaconda 3开辟了一个名为Beacon的环境专门用于训练Beacon模型。

创建环境只要在Anaconda Navigator中的Environments板块里点击create按钮,再选择Python版本号为3.6进行创建即可。

然后打开Anaconda终端,执行安装语句安装对应版本的tensorflow包和scipy包即可。

在终端里,安装包的命令是:

conda install package_name=version

但是这里踩了坑。不能先安装tensorflow再安装scipy。经测试,这样的话安装scipy时,会发现已经有一个较高版本的scipy了,疑似是tensorflow附加安装的。

由于Anaconda不允许有多个版本的相同包,则此时必须要卸载scipy包。

卸载命令是:

conda uninstall package_name

但此时卸载tensorflow附带安装的scipy会把tensorflow也卸载。

所以必须先安装scipy再安装tensorflow。

之后就是在我的Pycharm中准备运行代码了。

但是Pycharm使用不了我刚配置好的Beacon环境。

在Pycharm中的IDE设置里能检测到刚配置好的环境,但是输入了正确的python.exe路径还是总是提示“Conda可执行文件路径为空”。

据悉,Pycharm会默认去C盘找python.exe路径。

试了很多方法,最后唯一成功的方法是在IDE设置里点击“”全部显示“:

下拉点击全部显示

然后点击左上角的”+“:

点+

接着在”现有环境“处选择python.exe路径:

点现有环境

这样就可以让Pycharm在第一张图片中的界面里检测出来新配置的环境了。

调参数

调参数使用命令行。因为代码中配置了tf.flags。

这里的参数设置方法就是在Pycharm中使用终端。

在Pycharm中打开终端,输入以下命令就可以设置好参数并运行py程序:

python xxx.py --hyper-para_name1 value1 --hyper-para_name2 value2

如果终端显示“无法加载文件 ***\WindowsPowerShell\profile.ps1,因为在此系统上禁止运行脚本”,那么说明本机现用执行策略是“Restricted”。可按下图步骤进行修复:

修复WindowsPowerShell

第一次失败的训练

先运行cmatrix_generator.py。

设置了参数data_dir为存放三个数据集(train、validate、test)的位置。

参数nb_hop没有设置,默认为1。

成功在正确的目录里生成了邻接矩阵文件。

然后在此基础上运行了main_gpu.py。

一开始我以为device_id不用设置,想着可能系统会自动为模型分配GPU。但事实上不行。经查阅,本机只有一个显卡,显卡号自然为0,故设为0.

查看本机显卡号的方法如下:

  1. 打开资源管理器,进入C:\Program Files\NVIDIA Corporation\NVSMI
  2. 在此文件夹运行终端,键入nvidia-smi命令

执行后界面如下: 查看本机显卡号

可见设备号确实是0。

设置了同样的data_dir、output_dir、tensorboard_dir。

train_mode设置为True。表示我想训练模型。

其他超参数均保持默认,开始训练。

训练情况很不理想。

首先,训练很慢,看起来这种大数据集配上高差异的数据很耗算力。而且不知道跟选择LSTM而没有选择GRU有没有关系,毕竟GRU较节省算力。

平均0.6s训练一批(32个序列)。光训练集就1883批,约18min才可训练完。

其次,效果极其不理想。

loss始终维持在1.1附近,recall@10始终在0.05附近。

虽然随着epoch轮数的增加,这两个指标有微弱的好转,但实在微乎其微。

于是赶忙放弃训练。放弃训练前正在走epoch7,原本的文件夹从40M增加到了2.2G…

下一次训练要好好调参数!不能因为看起来默认数值很靠谱就按默认走。

不过可以肯定问题全出在参数上。毕竟数据集是作者给的,数据集肯定没问题。

第二次尝试:首次调参

据悉:

  1. LSTM比GRU效率低
  2. 高阶邻接矩阵去稀疏效果强,避免不必要的计算
  3. epoch走一个太多时间,根本走不完默认的15个
  4. 提高学习率(learning rate)可以提高收敛速度

所以,为了提高模型训练速度、提升模型训练效果,我修改了以下参数:

  • rnn_cell_type: GRU
  • nb_hop: 10(包括cmatrix_generator.py和main_gpu.py)
  • nb_epoch: 5
  • learning_rate: 0.01

第二次loss显著下降到0.8左右,召回率没有变,训练时间仅有微小减少。说明改参数是有正向效果的的,但远没有达到预期效果。

转战Google Colab

我的代码注释就是在Google Colab上写的,但是一直没有用它来运行代码。经学长推荐,正式转战Google Colab,以期获得更高的训练速度。在此感谢学长团队总结出的Colab的Tutorial。

把Colab当成自己的一台电脑就好。

连接到运行时后,点击左侧文件图标就可以看到这台“电脑”上的目录:

Colab目录

为了让自己的代码文件进入目录,可以采用把代码上传到Google云端硬盘,然后在Colab中挂载Google云端硬盘。这样Google云端硬盘的目录会出现在运行时环境的目录系统中。

在上传文件至Google云端硬盘后,可在Colab执行以下代码进行挂载:

from google.colab import drive
drive.mount('/content/drive')

这时候刷新文件界面就会看到新增加的“drive”文件夹。这级目录就对应于上述代码中的/content/drive这级目录。日后要访问Google云端硬盘的目录就只要以这个路径为根目录进行路径输入再正常访问就可以。

接下来是为Beacon配置Colab环境。Colab默认使用的都是较新的Python和Python库。我看它的Python是3.7的,跟所需的3.6过渡平滑,就没有修改版本。

在Colab中,所有语句前加上“!”就可以成为命令行语句。使用如下两个语句可以安装包、删除包和列出所有包:

!pip install package_name==version
!pip uninstall package_name
!pip list

Anaconda爱好者也可以运行以下代码:

!wget -c https://repo.continuum.io/archive/Anaconda3-5.1.0-Linux-x86_64.sh
!chmod +x Anaconda3-5.1.0-Linux-x86_64.sh
!bash ./Anaconda3-5.1.0-Linux-x86_64.sh -b -f -p /usr/local
 
import sys
sys.path.append('/usr/local/lib/python3.6/site-packages/')

然后用conda命令进行正常配置也行。但是我个人不太习惯conda附带安装依赖环境的做法,容易使原本的环境模糊化,在Colab上显得没必要。

使用以下这段代码将环境启用在目标目录:

import os
os.chdir('/content/drive/MyDrive/beacon-master')

如上,这里通过/content/drive这级目录转到了云盘上存储我的代码文件的地方。

然后运行:

!python cmatrix_generator.py --data_dir "/content/drive/MyDrive/beacon-input-and-output"

从而运行cmatrix_generator.py,产生邻接矩阵。这里依照学长的建议,为了复现论文,准备还是使用默认参数(邻接矩阵阶数为1)。输出的结果会被保存在谷歌云盘中。

main_gpu.py也按上述代码格式运行,并且参数保持默认。

其实最后的运行效果跟在本机上完全一样,甚至时间还花费了更长。

通过查看GPU资源使用情况,发现完全没有在使用GPU,而在用CPU(Colab的CPU居然比我电脑的差)。

这就使我百思不得其解了。后来也尝试过改代码,但是没有效果。

之后据悉,tensorflow在没有用:

with tf.device('/GPU:0'):

的类似语句进行指定的时候,默认用CPU进行训练,device_id也默认针对的是CPU。

如果像Beacon这样代码中没有显式地指定,必须卸载掉tensorflow包,再下载对应版本的tensorflow-gpu包,也就是tensorflow的GPU版本,这个版本会默认使用GPU进行训练。

第一次复现

参数全默认,又复现了一次。更换成GPU版本后,运算速度飙升,0.05s就能算一批,运算速度慢的问题彻底解决了。

最终结果差不多,然后train完之后进行了tune,最后进行了prediction,预测top10。

得到的预测文件如下:

预测文件

每一组都由一个Target和所有Target后面以空格隔开的是最后一个篮子中的物品,也就是我们希望预测到的结果。后面以|隔开的就是test集中出现过的物品。

Beacon为test集中的每一个物品都打了分,分越高表示Beacon越觉得它会出现在目标篮子中。上面这个预测文件中,每一个物品都按分数从高到低进行排列。

可以看到,test集中出现过的物品都是固定的那么些个。如果Target中有test集中出现过的物品,那么该物品的分数会非常高,并且肯定出现在top10中。所以模型本身是训练的不错的。

但是召回率低的原因也显而易见,很多Target中的物品test集中没有,换句话说,Beacon根本没办法为这些物品打分!

难题与解决思路

上网查、向学长请教,都是可以让自己受益匪浅的方法。

目前可以确定,首次尝试调参还是有不错的效果的,可以闲来无事玩玩,探究一下这些超参数对模型的影响。

心得收获

NN真的是一入深似海。还得钻研,入了门才发现其难,愈往而不知其所穷也!

下周计划

看看能不能搞定Beacon,反正关键是要解决只给test集里的物品打分的这个问题。初步想法是改test集,具体而言可以试试扩大test集。