Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions cuda_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ def locate_cuda():

post_args = [
"-arch=sm_60",
"-gencode=arch=compute_50,code=sm_50",
"-gencode=arch=compute_52,code=sm_52",
"-gencode=arch=compute_60,code=sm_60",
"-gencode=arch=compute_61,code=sm_61",
"-gencode=arch=compute_70,code=sm_70",
Expand Down
7 changes: 4 additions & 3 deletions examples/lastfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ def calculate_similar_artists(output_filename, model_name="als"):

# this is actually disturbingly expensive:
plays = plays.tocsr()
user_plays = plays.T.tocsr()

logging.debug("training model %s", model_name)
start = time.time()
model.fit(plays)
model.fit(user_plays)
logging.debug("trained model '%s' in %0.2fs", model_name, time.time() - start)

# write out similar artists by popularity
Expand Down Expand Up @@ -131,15 +132,15 @@ def calculate_recommendations(output_filename, model_name="als"):

# this is actually disturbingly expensive:
plays = plays.tocsr()
user_plays = plays.T.tocsr()

logging.debug("training model %s", model_name)
start = time.time()
model.fit(plays)
model.fit(user_plays)
logging.debug("trained model '%s' in %0.2fs", model_name, time.time() - start)

# generate recommendations for each user and write out to a file
start = time.time()
user_plays = plays.T.tocsr()
with tqdm.tqdm(total=len(users)) as progress:
with codecs.open(output_filename, "w", "utf8") as o:
for userid, username in enumerate(users):
Expand Down
4 changes: 3 additions & 1 deletion examples/movielens.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ def calculate_similar_movies(output_filename, model_name="als", min_rating=4.0,
else:
raise NotImplementedError("TODO: model %s" % model_name)

user_ratings = ratings.T.tocsr()

# train the model
log.debug("training model %s", model_name)
start = time.time()
model.fit(ratings)
model.fit(user_ratings)
log.debug("trained model '%s' in %s", model_name, time.time() - start)
log.debug("calculating top movies")

Expand Down
6 changes: 3 additions & 3 deletions implicit/_nearest_neighbours.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ cdef class NearestNeighboursScorer(object):


@cython.boundscheck(False)
def all_pairs_knn(items, unsigned int K=100, int num_threads=0, show_progress=True):
def all_pairs_knn(users, unsigned int K=100, int num_threads=0, show_progress=True):
""" Returns the top K nearest neighbours for each row in the matrix.
"""
items = items.tocsr()
users = items.T.tocsr()
users = users.tocsr()
items = users.T.tocsr()

cdef int item_count = items.shape[0]
cdef int i, u, index1, index2, j
Expand Down
12 changes: 6 additions & 6 deletions implicit/approximate_als.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ def __init__(
*args, random_state=random_state, **kwargs
)

def fit(self, Ciu, show_progress=True):
def fit(self, Cui, show_progress=True):
# nmslib can be a little chatty when first imported, disable some of
# the logging
logging.getLogger("nmslib").setLevel(logging.WARNING)
import nmslib

# train the model
super(NMSLibAlternatingLeastSquares, self).fit(Ciu, show_progress)
super(NMSLibAlternatingLeastSquares, self).fit(Cui, show_progress)

# create index for similar_items
if self.approximate_similar_items:
Expand Down Expand Up @@ -238,12 +238,12 @@ def __init__(
self.n_trees = n_trees
self.search_k = search_k

def fit(self, Ciu, show_progress=True):
def fit(self, Cui, show_progress=True):
# delay loading the annoy library in case its not installed here
import annoy

# train the model
super(AnnoyAlternatingLeastSquares, self).fit(Ciu, show_progress)
super(AnnoyAlternatingLeastSquares, self).fit(Cui, show_progress)

# build up an Annoy Index with all the item_factors (for calculating
# similar items)
Expand Down Expand Up @@ -377,11 +377,11 @@ def __init__(
*args, random_state=random_state, **kwargs
)

def fit(self, Ciu, show_progress=True):
def fit(self, Cui, show_progress=True):
import faiss

# train the model
super(FaissAlternatingLeastSquares, self).fit(Ciu, show_progress)
super(FaissAlternatingLeastSquares, self).fit(Cui, show_progress)

self.quantizer = faiss.IndexFlat(self.factors)

Expand Down
28 changes: 14 additions & 14 deletions implicit/cpu/als.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,46 +93,46 @@ def __init__(

check_blas_config()

def fit(self, item_users, show_progress=True):
"""Factorizes the item_users matrix.
def fit(self, user_items, show_progress=True):
"""Factorizes the user_items matrix.

After calling this method, the members 'user_factors' and 'item_factors' will be
initialized with a latent factor model of the input data.

The item_users matrix does double duty here. It defines which items are liked by which
users (P_iu in the original paper), as well as how much confidence we have that the user
liked the item (C_iu).
The user_items matrix does double duty here. It defines which items are liked by which
users (P_ui in the original paper), as well as how much confidence we have that the user
liked the item (C_ui).

The negative items are implicitly defined: This code assumes that positive items in the
item_users matrix means that the user liked the item. The negatives are left unset in this
user_items matrix means that the user liked the item. The negatives are left unset in this
sparse matrix: the library will assume that means Piu = 0 and Ciu = 1 for all these items.
Negative items can also be passed with a higher confidence value by passing a negative
value, indicating that the user disliked the item.

Parameters
----------
item_users: csr_matrix
user_items: csr_matrix
Matrix of confidences for the liked items. This matrix should be a csr_matrix where
the rows of the matrix are the item, the columns are the users that liked that item,
the rows of the matrix are the users, the columns are the items liked that user,
and the value is the confidence that the user liked the item.
show_progress : bool, optional
Whether to show a progress bar during fitting
"""
# initialize the random state
random_state = check_random_state(self.random_state)

Ciu = item_users
if not isinstance(Ciu, scipy.sparse.csr_matrix):
Cui = user_items
if not isinstance(Cui, scipy.sparse.csr_matrix):
s = time.time()
log.debug("Converting input to CSR format")
Ciu = Ciu.tocsr()
Cui = Cui.tocsr()
log.debug("Converted input to CSR in %.3fs", time.time() - s)

if Ciu.dtype != np.float32:
Ciu = Ciu.astype(np.float32)
if Cui.dtype != np.float32:
Cui = Cui.astype(np.float32)

s = time.time()
Cui = Ciu.T.tocsr()
Ciu = Cui.T.tocsr()
log.debug("Calculated transpose in %.3fs", time.time() - s)

items, users = Ciu.shape
Expand Down
19 changes: 8 additions & 11 deletions implicit/cpu/bpr.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,14 @@ class BayesianPersonalizedRanking(MatrixFactorizationBase):

@cython.cdivision(True)
@cython.boundscheck(False)
def fit(self, item_users, show_progress=True):
""" Factorizes the item_users matrix
def fit(self, user_items, show_progress=True):
""" Factorizes the user_items matrix

Parameters
----------
item_users: coo_matrix
Matrix of confidences for the liked items. This matrix should be a coo_matrix where
the rows of the matrix are the item, and the columns are the users that liked that item.
user_items: csr_matrix
Matrix of confidences for the liked items. This matrix should be a csr_matrix where
the rows of the matrix are the user, and the columns are the items liked by that user.
BPR ignores the weight value of the matrix right now - it treats non zero entries
as a binary signal that the user liked the item.
show_progress : bool, optional
Expand All @@ -137,15 +137,12 @@ class BayesianPersonalizedRanking(MatrixFactorizationBase):
rs = check_random_state(self.random_state)

# for now, all we handle is float 32 values
if item_users.dtype != np.float32:
item_users = item_users.astype(np.float32)
if user_items.dtype != np.float32:
user_items = user_items.astype(np.float32)

items, users = item_users.shape
users, items = user_items.shape

# We need efficient user lookup for case of removing own likes
# TODO: might make more sense to just changes inputs to be users by items instead
# but that would be a major breaking API change
user_items = item_users.T.tocsr()
if not user_items.has_sorted_indices:
user_items.sort_indices()

Expand Down
28 changes: 14 additions & 14 deletions implicit/gpu/als.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,27 @@ def __init__(
self.random_state = random_state
self.cg_steps = 3

def fit(self, item_users, show_progress=True):
"""Factorizes the item_users matrix.
def fit(self, user_items, show_progress=True):
"""Factorizes the user_items matrix.

After calling this method, the members 'user_factors' and 'item_factors' will be
initialized with a latent factor model of the input data.

The item_users matrix does double duty here. It defines which items are liked by which
users (P_iu in the original paper), as well as how much confidence we have that the user
liked the item (C_iu).
The user_items matrix does double duty here. It defines which items are liked by which
users (P_ui in the original paper), as well as how much confidence we have that the user
liked the item (C_ui.

The negative items are implicitly defined: This code assumes that positive items in the
item_users matrix means that the user liked the item. The negatives are left unset in this
user_items matrix means that the user liked the item. The negatives are left unset in this
sparse matrix: the library will assume that means Piu = 0 and Ciu = 1 for all these items.
Negative items can also be passed with a higher confidence value by passing a negative
value, indicating that the user disliked the item.

Parameters
----------
item_users: csr_matrix
user_items: csr_matrix
Matrix of confidences for the liked items. This matrix should be a csr_matrix where
the rows of the matrix are the item, the columns are the users that liked that item,
the rows of the matrix are the user, the columns are the items liked by that user,
and the value is the confidence that the user liked the item.
show_progress : bool, optional
Whether to show a progress bar during fitting
Expand All @@ -96,18 +96,18 @@ def fit(self, item_users, show_progress=True):
random_state = check_random_state(self.random_state)

# TODO: allow passing in cupy arrays on gpu
Ciu = item_users
if not isinstance(Ciu, scipy.sparse.csr_matrix):
Cui = user_items
if not isinstance(Cui, scipy.sparse.csr_matrix):
s = time.time()
log.debug("Converting input to CSR format")
Ciu = Ciu.tocsr()
Cui = Cui.tocsr()
log.debug("Converted input to CSR in %.3fs", time.time() - s)

if Ciu.dtype != np.float32:
Ciu = Ciu.astype(np.float32)
if Cui.dtype != np.float32:
Cui = Cui.astype(np.float32)

s = time.time()
Cui = Ciu.T.tocsr()
Ciu = Cui.T.tocsr()
log.debug("Calculated transpose in %.3fs", time.time() - s)

items, users = Ciu.shape
Expand Down
21 changes: 11 additions & 10 deletions implicit/lmf.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,15 @@ class LogisticMatrixFactorization(MatrixFactorizationBase):

@cython.cdivision(True)
@cython.boundscheck(False)
def fit(self, item_users, show_progress=True):
""" Factorizes the item_users matrix
def fit(self, user_items, show_progress=True):
""" Factorizes the user_items matrix

Parameters
----------
item_users: coo_matrix
Matrix of confidences for the liked items. This matrix should be a coo_matrix where
the rows of the matrix are the item, and the columns are the users that liked that item.
user_items: csr_matrix
Matrix of confidences for the liked items. This matrix should be a csr_matrix where
the rows of the matrix are the user, and the columns are the items that are liked by
the user.
BPR ignores the weight value of the matrix right now - it treats non zero entries
as a binary signal that the user liked the item.
show_progress : bool, optional
Expand All @@ -133,13 +134,13 @@ class LogisticMatrixFactorization(MatrixFactorizationBase):
rs = check_random_state(self.random_state)

# for now, all we handle is float 32 values
if item_users.dtype != np.float32:
item_users = item_users.astype(np.float32)
if user_items.dtype != np.float32:
user_items = user_items.astype(np.float32)

items, users = item_users.shape
users, items = user_items.shape

item_users = item_users.tocsr()
user_items = item_users.T.tocsr()
user_items = user_items.tocsr()
item_users = user_items.T.tocsr()

if not item_users.has_sorted_indices:
item_users.sort_indices()
Expand Down
6 changes: 3 additions & 3 deletions implicit/recommender_base.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ class RecommenderBase(object):
__metaclass__ = ABCMeta

@abstractmethod
def fit(self, item_users):
def fit(self, user_items):
"""
Trains the model on a sparse matrix of item/user/weight

Parameters
----------
item_user : csr_matrix
A matrix of shape (number_of_items, number_of_users). The nonzero
user_items : csr_matrix
A matrix of shape (number_of_users, number_of_items). The nonzero
entries in this matrix are the items that are liked by each user.
The values are how confident you are that the item is liked by the user.
"""
Expand Down
14 changes: 7 additions & 7 deletions tests/als_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def test_factorize(use_native, use_gpu, use_cg, dtype):
random_state=42,
)
model.fit(user_items, show_progress=False)
rows, cols = model.item_factors, model.user_factors
rows, cols = model.user_factors, model.item_factors

if use_gpu:
rows, cols = rows.to_numpy(), cols.to_numpy()
Expand Down Expand Up @@ -154,8 +154,8 @@ def test_explain():
],
dtype=np.float64,
)
user_items = counts * 2
item_users = user_items.T
item_users = counts * 2
user_items = item_users.T.tocsr()

model = AlternatingLeastSquares(
factors=4,
Expand All @@ -174,17 +174,17 @@ def test_explain():
# TODO: this doesn't quite work with N=10 (because we returns items that should have been
# filtered with large negative score?) also seems like the dtype is different between
# recalculate and not
ids, scores = model.recommend(userid, item_users, N=5)
ids, scores = model.recommend(userid, user_items, N=3)
recalculated_ids, recalculated_scores = model.recommend(
userid, item_users, N=5, recalculate_user=True
userid, user_items, N=3, recalculate_user=True
)
for item1, score1, item2, score2 in zip(ids, scores, recalculated_ids, recalculated_scores):
assert item1 == item2
assert pytest.approx(score1, abs=1e-4) == score2

# Assert explanation makes sense
top_rec, score = recalculated_ids[0], recalculated_scores[0]
score_explained, contributions, W = model.explain(userid, item_users, itemid=top_rec)
score_explained, contributions, W = model.explain(userid, user_items, itemid=top_rec)
scores = [s for _, s in contributions]
items = [i for i, _ in contributions]
assert pytest.approx(score, abs=1e-4) == score_explained
Expand All @@ -196,7 +196,7 @@ def test_explain():

# Assert explanation with precomputed user weights is correct
top_score_explained, top_contributions, W = model.explain(
userid, item_users, itemid=top_rec, user_weights=W, N=2
userid, user_items, itemid=top_rec, user_weights=W, N=2
)
top_scores = [s for _, s in top_contributions]
top_items = [i for i, _ in top_contributions]
Expand Down
4 changes: 2 additions & 2 deletions tests/knn_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ def test_all_pairs_knn():
counts = implicit.nearest_neighbours.tfidf_weight(counts).tocsr()

# compute all neighbours using matrix dot product
all_neighbours = counts.dot(counts.T).tocsr()
all_neighbours = counts.T.dot(counts).tocsr()
K = 3
knn = implicit.nearest_neighbours.all_pairs_knn(counts, K, show_progress=False).tocsr()

for rowid in range(counts.shape[0]):
for rowid in range(counts.shape[1]):
# make sure values match
for colid, data in zip(knn[rowid].indices, knn[rowid].data):
pytest.approx(all_neighbours[rowid, colid]) == data
Expand Down