Skip to content

Commit

Permalink
support save/load in PySpark's ALS
Browse files Browse the repository at this point in the history
  • Loading branch information
mengxr committed Feb 27, 2015
1 parent 7c99a01 commit 282ec8d
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 8 deletions.
16 changes: 10 additions & 6 deletions docs/mllib-collaborative-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ val MSE = ratesAndPreds.map { case ((user, product), (r1, r2)) =>
}.mean()
println("Mean Squared Error = " + MSE)

model.save("myModelPath")
val sameModel = MatrixFactorizationModel.load("myModelPath")
// Save and load model
model.save(sc, "myModelPath")
val sameModel = MatrixFactorizationModel.load(sc, "myModelPath")
{% endhighlight %}

If the rating matrix is derived from another source of information (e.g., it is inferred from
Expand Down Expand Up @@ -186,8 +187,9 @@ public class CollaborativeFiltering {
).rdd()).mean();
System.out.println("Mean Squared Error = " + MSE);

model.save("myModelPath");
MatrixFactorizationModel sameModel = MatrixFactorizationModel.load("myModelPath");
// Save and load model
model.save(sc.sc(), "myModelPath");
MatrixFactorizationModel sameModel = MatrixFactorizationModel.load(sc.sc(), "myModelPath");
}
}
{% endhighlight %}
Expand All @@ -198,8 +200,6 @@ In the following example we load rating data. Each row consists of a user, a pro
We use the default ALS.train() method which assumes ratings are explicit. We evaluate the
recommendation by measuring the Mean Squared Error of rating prediction.

Note that the Python API does not yet support model save/load but will in the future.

{% highlight python %}
from pyspark.mllib.recommendation import ALS, Rating

Expand All @@ -218,6 +218,10 @@ predictions = model.predictAll(testdata).map(lambda r: ((r[0], r[1]), r[2]))
ratesAndPreds = ratings.map(lambda r: ((r[0], r[1]), r[2])).join(predictions)
MSE = ratesAndPreds.map(lambda r: (r[1][0] - r[1][1])**2).reduce(lambda x, y: x + y) / ratesAndPreds.count()
print("Mean Squared Error = " + str(MSE))

# Save and load model
model.save("myModelPath")
sameModel = MatrixFactorizationModel.load(sc, "myModelPath")
{% endhighlight %}

If the rating matrix is derived from other source of information (i.e., it is inferred from other
Expand Down
20 changes: 18 additions & 2 deletions python/pyspark/mllib/recommendation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

from pyspark import SparkContext
from pyspark.rdd import RDD
from pyspark.mllib.common import JavaModelWrapper, callMLlibFunc
from pyspark.mllib.common import JavaModelWrapper, callMLlibFunc, inherit_doc
from pyspark.mllib.util import Saveable, JavaLoader

__all__ = ['MatrixFactorizationModel', 'ALS', 'Rating']

Expand All @@ -39,7 +40,8 @@ def __reduce__(self):
return Rating, (int(self.user), int(self.product), float(self.rating))


class MatrixFactorizationModel(JavaModelWrapper):
@inherit_doc
class MatrixFactorizationModel(JavaModelWrapper, Saveable, JavaLoader):

"""A matrix factorisation model trained by regularized alternating
least-squares.
Expand Down Expand Up @@ -81,6 +83,17 @@ class MatrixFactorizationModel(JavaModelWrapper):
>>> model = ALS.trainImplicit(ratings, 1, nonnegative=True, seed=10)
>>> model.predict(2,2)
0.43...
>>> import os, tempfile
>>> path = tempfile.mkdtemp()
>>> model.save(sc, path)
>>> sameModel = MatrixFactorizationModel.load(sc, path)
>>> sameModel.predict(2,2)
0.43...
>>> try:
... os.removedirs(path)
... except:
... pass
"""
def predict(self, user, product):
return self._java_model.predict(int(user), int(product))
Expand All @@ -98,6 +111,9 @@ def userFeatures(self):
def productFeatures(self):
return self.call("getProductFeatures")

def save(self, sc, path):
self.call("save", sc._jsc.sc(), path)


class ALS(object):

Expand Down
58 changes: 58 additions & 0 deletions python/pyspark/mllib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,64 @@ def loadLabeledPoints(sc, path, minPartitions=None):
return callMLlibFunc("loadLabeledPoints", sc, path, minPartitions)


class Saveable(object):
"""
Mixin for models and transformers which may be saved as files.
"""

def save(self, sc, path):
"""
Save this model to the given path.
This saves:
* human-readable (JSON) model metadata to path/metadata/
* Parquet formatted data to path/data/
The model may be loaded using py:meth:`Loader.load`.
:param sc: Spark context used to save model data.
:param path: Path specifying the directory in which to save
this model. This directory and any intermediate
directory will be created if needed.
"""
raise NotImplementedError


class Loader(object):
"""
Mixin for classes which can load saved models from files.
"""

@classmethod
def load(cls, sc, path):
"""
Load a model from the given path. The model should have been
saved using py:meth:`Saveable.save`.
:param sc: Spark context used for loading model files.
:param path: Path specifying the directory to which the model
was saved.
:return: model instance
"""
raise NotImplemented


class JavaLoader(Loader):
"""
Mixin for classes which can load saved models using its Scala
implementation.
"""

@classmethod
def load(cls, sc, path):
java_package = cls.__module__.replace("pyspark", "org.apache.spark")
java_class = ".".join([java_package, cls.__name__])
java_obj = sc._jvm
for name in java_class.split("."):
java_obj = getattr(java_obj, name)
return cls(java_obj.load(sc._jsc.sc(), path))


def _test():
import doctest
from pyspark.context import SparkContext
Expand Down

0 comments on commit 282ec8d

Please sign in to comment.