fdde7d0aa0
## What changes were proposed in this pull request? Issue: Omitting the full classpath can cause problems when calling JVM methods or classes from pyspark. This PR: Changed all uses of jvm.X in pyspark.ml and pyspark.mllib to use full classpath for X ## How was this patch tested? Existing unit tests. Manual testing in an environment where this was an issue. Author: Joseph K. Bradley <joseph@databricks.com> Closes #14023 from jkbradley/SPARK-16348.
334 lines
11 KiB
Python
334 lines
11 KiB
Python
#
|
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to You 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.
|
|
#
|
|
|
|
import array
|
|
from collections import namedtuple
|
|
|
|
from pyspark import SparkContext, since
|
|
from pyspark.rdd import RDD
|
|
from pyspark.mllib.common import JavaModelWrapper, callMLlibFunc, inherit_doc
|
|
from pyspark.mllib.util import JavaLoader, JavaSaveable
|
|
from pyspark.sql import DataFrame
|
|
|
|
__all__ = ['MatrixFactorizationModel', 'ALS', 'Rating']
|
|
|
|
|
|
class Rating(namedtuple("Rating", ["user", "product", "rating"])):
|
|
"""
|
|
Represents a (user, product, rating) tuple.
|
|
|
|
>>> r = Rating(1, 2, 5.0)
|
|
>>> (r.user, r.product, r.rating)
|
|
(1, 2, 5.0)
|
|
>>> (r[0], r[1], r[2])
|
|
(1, 2, 5.0)
|
|
|
|
.. versionadded:: 1.2.0
|
|
"""
|
|
|
|
def __reduce__(self):
|
|
return Rating, (int(self.user), int(self.product), float(self.rating))
|
|
|
|
|
|
@inherit_doc
|
|
class MatrixFactorizationModel(JavaModelWrapper, JavaSaveable, JavaLoader):
|
|
|
|
"""A matrix factorisation model trained by regularized alternating
|
|
least-squares.
|
|
|
|
>>> r1 = (1, 1, 1.0)
|
|
>>> r2 = (1, 2, 2.0)
|
|
>>> r3 = (2, 1, 2.0)
|
|
>>> ratings = sc.parallelize([r1, r2, r3])
|
|
>>> model = ALS.trainImplicit(ratings, 1, seed=10)
|
|
>>> model.predict(2, 2)
|
|
0.4...
|
|
|
|
>>> testset = sc.parallelize([(1, 2), (1, 1)])
|
|
>>> model = ALS.train(ratings, 2, seed=0)
|
|
>>> model.predictAll(testset).collect()
|
|
[Rating(user=1, product=1, rating=1.0...), Rating(user=1, product=2, rating=1.9...)]
|
|
|
|
>>> model = ALS.train(ratings, 4, seed=10)
|
|
>>> model.userFeatures().collect()
|
|
[(1, array('d', [...])), (2, array('d', [...]))]
|
|
|
|
>>> model.recommendUsers(1, 2)
|
|
[Rating(user=2, product=1, rating=1.9...), Rating(user=1, product=1, rating=1.0...)]
|
|
>>> model.recommendProducts(1, 2)
|
|
[Rating(user=1, product=2, rating=1.9...), Rating(user=1, product=1, rating=1.0...)]
|
|
>>> model.rank
|
|
4
|
|
|
|
>>> first_user = model.userFeatures().take(1)[0]
|
|
>>> latents = first_user[1]
|
|
>>> len(latents)
|
|
4
|
|
|
|
>>> model.productFeatures().collect()
|
|
[(1, array('d', [...])), (2, array('d', [...]))]
|
|
|
|
>>> first_product = model.productFeatures().take(1)[0]
|
|
>>> latents = first_product[1]
|
|
>>> len(latents)
|
|
4
|
|
|
|
>>> products_for_users = model.recommendProductsForUsers(1).collect()
|
|
>>> len(products_for_users)
|
|
2
|
|
>>> products_for_users[0]
|
|
(1, (Rating(user=1, product=2, rating=...),))
|
|
|
|
>>> users_for_products = model.recommendUsersForProducts(1).collect()
|
|
>>> len(users_for_products)
|
|
2
|
|
>>> users_for_products[0]
|
|
(1, (Rating(user=2, product=1, rating=...),))
|
|
|
|
>>> model = ALS.train(ratings, 1, nonnegative=True, seed=10)
|
|
>>> model.predict(2, 2)
|
|
3.73...
|
|
|
|
>>> df = sqlContext.createDataFrame([Rating(1, 1, 1.0), Rating(1, 2, 2.0), Rating(2, 1, 2.0)])
|
|
>>> model = ALS.train(df, 1, nonnegative=True, seed=10)
|
|
>>> model.predict(2, 2)
|
|
3.73...
|
|
|
|
>>> model = ALS.trainImplicit(ratings, 1, nonnegative=True, seed=10)
|
|
>>> model.predict(2, 2)
|
|
0.4...
|
|
|
|
>>> import os, tempfile
|
|
>>> path = tempfile.mkdtemp()
|
|
>>> model.save(sc, path)
|
|
>>> sameModel = MatrixFactorizationModel.load(sc, path)
|
|
>>> sameModel.predict(2, 2)
|
|
0.4...
|
|
>>> sameModel.predictAll(testset).collect()
|
|
[Rating(...
|
|
>>> from shutil import rmtree
|
|
>>> try:
|
|
... rmtree(path)
|
|
... except OSError:
|
|
... pass
|
|
|
|
.. versionadded:: 0.9.0
|
|
"""
|
|
@since("0.9.0")
|
|
def predict(self, user, product):
|
|
"""
|
|
Predicts rating for the given user and product.
|
|
"""
|
|
return self._java_model.predict(int(user), int(product))
|
|
|
|
@since("0.9.0")
|
|
def predictAll(self, user_product):
|
|
"""
|
|
Returns a list of predicted ratings for input user and product
|
|
pairs.
|
|
"""
|
|
assert isinstance(user_product, RDD), "user_product should be RDD of (user, product)"
|
|
first = user_product.first()
|
|
assert len(first) == 2, "user_product should be RDD of (user, product)"
|
|
user_product = user_product.map(lambda u_p: (int(u_p[0]), int(u_p[1])))
|
|
return self.call("predict", user_product)
|
|
|
|
@since("1.2.0")
|
|
def userFeatures(self):
|
|
"""
|
|
Returns a paired RDD, where the first element is the user and the
|
|
second is an array of features corresponding to that user.
|
|
"""
|
|
return self.call("getUserFeatures").mapValues(lambda v: array.array('d', v))
|
|
|
|
@since("1.2.0")
|
|
def productFeatures(self):
|
|
"""
|
|
Returns a paired RDD, where the first element is the product and the
|
|
second is an array of features corresponding to that product.
|
|
"""
|
|
return self.call("getProductFeatures").mapValues(lambda v: array.array('d', v))
|
|
|
|
@since("1.4.0")
|
|
def recommendUsers(self, product, num):
|
|
"""
|
|
Recommends the top "num" number of users for a given product and
|
|
returns a list of Rating objects sorted by the predicted rating in
|
|
descending order.
|
|
"""
|
|
return list(self.call("recommendUsers", product, num))
|
|
|
|
@since("1.4.0")
|
|
def recommendProducts(self, user, num):
|
|
"""
|
|
Recommends the top "num" number of products for a given user and
|
|
returns a list of Rating objects sorted by the predicted rating in
|
|
descending order.
|
|
"""
|
|
return list(self.call("recommendProducts", user, num))
|
|
|
|
def recommendProductsForUsers(self, num):
|
|
"""
|
|
Recommends the top "num" number of products for all users. The
|
|
number of recommendations returned per user may be less than "num".
|
|
"""
|
|
return self.call("wrappedRecommendProductsForUsers", num)
|
|
|
|
def recommendUsersForProducts(self, num):
|
|
"""
|
|
Recommends the top "num" number of users for all products. The
|
|
number of recommendations returned per product may be less than
|
|
"num".
|
|
"""
|
|
return self.call("wrappedRecommendUsersForProducts", num)
|
|
|
|
@property
|
|
@since("1.4.0")
|
|
def rank(self):
|
|
"""Rank for the features in this model"""
|
|
return self.call("rank")
|
|
|
|
@classmethod
|
|
@since("1.3.1")
|
|
def load(cls, sc, path):
|
|
"""Load a model from the given path"""
|
|
model = cls._load_java(sc, path)
|
|
wrapper = sc._jvm.org.apache.spark.mllib.api.python.MatrixFactorizationModelWrapper(model)
|
|
return MatrixFactorizationModel(wrapper)
|
|
|
|
|
|
class ALS(object):
|
|
"""Alternating Least Squares matrix factorization
|
|
|
|
.. versionadded:: 0.9.0
|
|
"""
|
|
|
|
@classmethod
|
|
def _prepare(cls, ratings):
|
|
if isinstance(ratings, RDD):
|
|
pass
|
|
elif isinstance(ratings, DataFrame):
|
|
ratings = ratings.rdd
|
|
else:
|
|
raise TypeError("Ratings should be represented by either an RDD or a DataFrame, "
|
|
"but got %s." % type(ratings))
|
|
first = ratings.first()
|
|
if isinstance(first, Rating):
|
|
pass
|
|
elif isinstance(first, (tuple, list)):
|
|
ratings = ratings.map(lambda x: Rating(*x))
|
|
else:
|
|
raise TypeError("Expect a Rating or a tuple/list, but got %s." % type(first))
|
|
return ratings
|
|
|
|
@classmethod
|
|
@since("0.9.0")
|
|
def train(cls, ratings, rank, iterations=5, lambda_=0.01, blocks=-1, nonnegative=False,
|
|
seed=None):
|
|
"""
|
|
Train a matrix factorization model given an RDD of ratings by users
|
|
for a subset of products. The ratings matrix is approximated as the
|
|
product of two lower-rank matrices of a given rank (number of
|
|
features). To solve for these features, ALS is run iteratively with
|
|
a configurable level of parallelism.
|
|
|
|
:param ratings:
|
|
RDD of `Rating` or (userID, productID, rating) tuple.
|
|
:param rank:
|
|
Rank of the feature matrices computed (number of features).
|
|
:param iterations:
|
|
Number of iterations of ALS.
|
|
(default: 5)
|
|
:param lambda_:
|
|
Regularization parameter.
|
|
(default: 0.01)
|
|
:param blocks:
|
|
Number of blocks used to parallelize the computation. A value
|
|
of -1 will use an auto-configured number of blocks.
|
|
(default: -1)
|
|
:param nonnegative:
|
|
A value of True will solve least-squares with nonnegativity
|
|
constraints.
|
|
(default: False)
|
|
:param seed:
|
|
Random seed for initial matrix factorization model. A value
|
|
of None will use system time as the seed.
|
|
(default: None)
|
|
"""
|
|
model = callMLlibFunc("trainALSModel", cls._prepare(ratings), rank, iterations,
|
|
lambda_, blocks, nonnegative, seed)
|
|
return MatrixFactorizationModel(model)
|
|
|
|
@classmethod
|
|
@since("0.9.0")
|
|
def trainImplicit(cls, ratings, rank, iterations=5, lambda_=0.01, blocks=-1, alpha=0.01,
|
|
nonnegative=False, seed=None):
|
|
"""
|
|
Train a matrix factorization model given an RDD of 'implicit
|
|
preferences' of users for a subset of products. The ratings matrix
|
|
is approximated as the product of two lower-rank matrices of a
|
|
given rank (number of features). To solve for these features, ALS
|
|
is run iteratively with a configurable level of parallelism.
|
|
|
|
:param ratings:
|
|
RDD of `Rating` or (userID, productID, rating) tuple.
|
|
:param rank:
|
|
Rank of the feature matrices computed (number of features).
|
|
:param iterations:
|
|
Number of iterations of ALS.
|
|
(default: 5)
|
|
:param lambda_:
|
|
Regularization parameter.
|
|
(default: 0.01)
|
|
:param blocks:
|
|
Number of blocks used to parallelize the computation. A value
|
|
of -1 will use an auto-configured number of blocks.
|
|
(default: -1)
|
|
:param alpha:
|
|
A constant used in computing confidence.
|
|
(default: 0.01)
|
|
:param nonnegative:
|
|
A value of True will solve least-squares with nonnegativity
|
|
constraints.
|
|
(default: False)
|
|
:param seed:
|
|
Random seed for initial matrix factorization model. A value
|
|
of None will use system time as the seed.
|
|
(default: None)
|
|
"""
|
|
model = callMLlibFunc("trainImplicitALSModel", cls._prepare(ratings), rank,
|
|
iterations, lambda_, blocks, alpha, nonnegative, seed)
|
|
return MatrixFactorizationModel(model)
|
|
|
|
|
|
def _test():
|
|
import doctest
|
|
import pyspark.mllib.recommendation
|
|
from pyspark.sql import SQLContext
|
|
globs = pyspark.mllib.recommendation.__dict__.copy()
|
|
sc = SparkContext('local[4]', 'PythonTest')
|
|
globs['sc'] = sc
|
|
globs['sqlContext'] = SQLContext(sc)
|
|
(failure_count, test_count) = doctest.testmod(globs=globs, optionflags=doctest.ELLIPSIS)
|
|
globs['sc'].stop()
|
|
if failure_count:
|
|
exit(-1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_test()
|