All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.intel.analytics.zoo.models.recommendation.SessionRecommender.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2018 Analytics Zoo Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.intel.analytics.zoo.models.recommendation

import com.intel.analytics.bigdl.dataset.{Sample, TensorSample}
import com.intel.analytics.bigdl.nn.Graph.ModuleNode
import com.intel.analytics.bigdl.nn.Sum
import com.intel.analytics.bigdl.nn.abstractnn.AbstractModule
import com.intel.analytics.bigdl.optim.LocalPredictor
import com.intel.analytics.bigdl.tensor.Tensor
import com.intel.analytics.bigdl.tensor.TensorNumericMath.TensorNumeric
import com.intel.analytics.bigdl.utils.Shape
import com.intel.analytics.zoo.models.common.ZooModel
import com.intel.analytics.zoo.pipeline.api.keras.layers._
import com.intel.analytics.zoo.pipeline.api.keras.models.Model
import org.apache.spark.rdd.RDD

import scala.reflect.ClassTag

/**
 * The session based recommender model.
 *
 * @param itemCount       The number of distinct items. Positive integer.
 * @param itemEmbed       The output size of embedding layer. Positive integer.
 * @param mlpHiddenLayers Units of hidden layers for the mlp model. Array of positive integers.
 * @param sessionLength   The max number of items in the sequence of a session
 * @param rnnHiddenLayers Units of hidden layers for the mlp model. Array of positive integers.
 * @param includeHistory  Whether to include purchase history. Boolean. Default is true.
 * @param historyLength   The max number of items in the sequence of historical purchase
 */
class SessionRecommender[T: ClassTag](
      val itemCount: Int,
      val itemEmbed: Int = 100,
      val rnnHiddenLayers: Array[Int] = Array(40, 20),
      val sessionLength: Int = 0,
      val includeHistory: Boolean = false,
      val mlpHiddenLayers: Array[Int] = Array(40, 20),
      val historyLength: Int = 0)(implicit ev: TensorNumeric[T]) extends Recommender[T] {

  override def buildModel(): AbstractModule[Tensor[T], Tensor[T], T] = {
    val inputRnn: ModuleNode[T] = Input(inputShape = Shape(sessionLength))

    // item embedding layer
    val sessionTable = Embedding[T](
      itemCount + 1, itemEmbed, "normal", inputLength = sessionLength).inputs(inputRnn)

    // rnn part
    var gru = GRU[T](rnnHiddenLayers(0), returnSequences = true).inputs(sessionTable)
    for (i <- 1 until rnnHiddenLayers.length - 1) {
      gru = GRU[T](rnnHiddenLayers(i), returnSequences = true).inputs(gru)
    }
    val gruLast = GRU[T](rnnHiddenLayers.last, returnSequences = false).inputs(gru)
    val rnn = Dense[T](itemCount).inputs(gruLast)

    // mlp part
    if (includeHistory) {
      val inputMlp: ModuleNode[T] = Input(inputShape = Shape(historyLength))
      val hisTable = Embedding[T](itemCount + 1, itemEmbed, inputLength = historyLength)
        .inputs(inputMlp)
      val sum = new KerasLayerWrapper[T](Sum[T](2)).inputs(hisTable)
      val flatten = Flatten().inputs(sum)

      var mlp = Dense(mlpHiddenLayers(0), activation = "relu").inputs(flatten)
      for (i <- 1 until mlpHiddenLayers.length) {
        mlp = Dense(mlpHiddenLayers(i), activation = "relu").inputs(mlp)
      }
      val mlpLast = Dense(itemCount).inputs(mlp)
      // combine rnn and mlp
      val merged = Merge.merge[T](List(rnn, mlpLast), "sum")
      val out = Activation("softmax").inputs(merged)
      Model(Array(inputRnn, inputMlp), out).asInstanceOf[AbstractModule[Tensor[T], Tensor[T], T]]
    }
    else {
      val out = Activation("softmax").inputs(rnn)
      Model(inputRnn, out).asInstanceOf[AbstractModule[Tensor[T], Tensor[T], T]]
    }
  }

  private def topk(_output: Tensor[T],
                   maxItems: Int,
                   zeroBasedLabel: Boolean = true): Array[(Int, Float)] = {

    val results: (Tensor[T], Tensor[T]) = _output.topk(maxItems, 1, false)

    val recommends = (1 to maxItems).map { i =>
      val zeroBase = if (zeroBasedLabel) 1 else 0
      val predict = (results._2.valueAt(i).asInstanceOf[Float] - zeroBase).toInt
      val probability = results._1.valueAt(i).asInstanceOf[Float]
      (predict, probability)
    }
    recommends.toArray
  }

  /**
   * recommend for sessions given rdd of samples
   *
   * @param sessions: rdd of samples
   * @param maxItems: Number of items to be recommended to each user. Positive integer.
   * @param zeroBasedLabel: True if data starts from 0, False if data starts from 1
   * @return rdd of array of (item, probability)
   *
   */
  def recommendForSession(sessions: RDD[Sample[T]],
                          maxItems: Int,
                          zeroBasedLabel: Boolean): RDD[Array[(Int, Float)]] = {
    val raw = predict(sessions)
    raw.map(x => topk(x.toTensor[T], maxItems, zeroBasedLabel))
  }

  /**
   * recommend for sessions given array of samples
   *
   * @param sessions: array of samples
   * @param maxItems: Number of items to be recommended to each user. Positive integer.
   * @param zeroBasedLabel: True if data starts from 0, False if data starts from 1
   * @return array of array of (item, probability)
   *
   */
  def recommendForSession(sessions: Array[Sample[T]],
                          maxItems: Int,
                          zeroBasedLabel: Boolean): Array[Array[(Int, Float)]] = {
    val raw = LocalPredictor(this).predict(sessions)
    raw.map(x => topk(x.toTensor[T], maxItems, zeroBasedLabel))
  }


  override def recommendForUser(featureRdd: RDD[UserItemFeature[T]],
                                maxItems: Int): RDD[UserItemPrediction] = {
    throw new UnsupportedOperationException(s"recommendForUser: Unsupported for SessionRecommender")
  }

  override def recommendForItem(featureRdd: RDD[UserItemFeature[T]],
                                maxItems: Int): RDD[UserItemPrediction] = {
    throw new UnsupportedOperationException(s"recommendForItem: Unsupported for SessionRecommender")
  }

  override def predictUserItemPair(featureRdd: RDD[UserItemFeature[T]]): RDD[UserItemPrediction] = {
    throw new UnsupportedOperationException(
      s"predictUserItemPair: Unsupported for SessionRecommender")
  }

}

object SessionRecommender {
  /**
   * The factory method to create a SessionRecommender instance.
   *
   * @param itemCount       The number of distinct items. Positive integer.
   * @param itemEmbed       The output size of embedding layer. Positive integer.
   * @param mlpHiddenLayers Units of hidden layers for the mlp model. Array of positive integers.
   * @param sessionLength       The max number of items in the sequence of a session
   * @param rnnHiddenLayers Units of hidden layers for the mlp model. Array of positive integers.
   * @param includeHistory  Whether to include purchase history. Boolean. Default is true.
   * @param historyLength       The max number of items in the sequence of historical purchase
   */
  def apply[@specialized(Float, Double) T: ClassTag](
      itemCount: Int,
      itemEmbed: Int = 100,
      rnnHiddenLayers: Array[Int] = Array(40, 20),
      sessionLength: Int = 0,
      includeHistory: Boolean = false,
      mlpHiddenLayers: Array[Int] = Array(40, 20),
      historyLength: Int = 0)(implicit ev: TensorNumeric[T]): SessionRecommender[T] = {
    require(sessionLength > 0, s"sessionLength should align with input features")
    if (includeHistory) {
      require(historyLength > 0, s"historyLength should align with input features")
    }

    new SessionRecommender[T](
      itemCount,
      itemEmbed,
      rnnHiddenLayers,
      sessionLength,
      includeHistory,
      mlpHiddenLayers,
      historyLength
    ).build()
  }

  /**
   * Load an existing SessionRecommender model (with weights).
   *
   * @param path       The path for the pre-defined model.
   *                   Local file system, HDFS and Amazon S3 are supported.
   *                   HDFS path should be like "hdfs://[host]:[port]/xxx".
   *                   Amazon S3 path should be like "s3a://bucket/xxx".
   * @param weightPath The path for pre-trained weights if any. Default is null.
   * @tparam T Numeric type of parameter(e.g. weight, bias). Only support float/double now.
   */
  def loadModel[T: ClassTag](
      path: String,
      weightPath: String = null)(implicit ev: TensorNumeric[T]): SessionRecommender[T] = {
    ZooModel.loadModel(path, weightPath).asInstanceOf[SessionRecommender[T]]
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy