Skip to content

Cosine similarity ZeroDivisionError #495

Description

@Gabriel-Kissin

Firstly, thanks for the great library!

I came across an issue while doing a large RandomizedSearchCV which was raising a ZeroDivisionError. This turned out to be because one of the users has all zero ratings, and cosine similarity was being used in that iteration, so obviously the denominator was zero. However it was tricky to debug because it wasn't immediately clear which similarity measure was being used when the error was raised. Because RandomizedSearchCV is parallelised, the traceback wasn't helpful.

MRE:

from surprise import Dataset, Reader, KNNBasic
import pandas as pd

df = pd.DataFrame([
    ['u1', 'i1', 0],  # user u1 only has 0 ratings
    ['u2', 'i1', 1],
    ['u2', 'i2', 0],
])
# df[2] = df[2] + 1 # uncommenting this line fixes the issue

reader = Reader(rating_scale=(df[2].min(), df[2].max()))
data = Dataset.load_from_df(df, reader)
trainset = data.build_full_trainset()

algo = KNNBasic(sim_options={'name': 'cosine'})
algo.fit(trainset)

Suggestion 1: clear error message

Catch the case that the denominator is zero and give a clear error message:

change similarities.pyx L76C1-L86C1 from

    for xi in range(n_x):
        sim[xi, xi] = 1
        for xj in range(xi + 1, n_x):
            if freq[xi, xj] < min_sprt:
                sim[xi, xj] = 0
            else:
                denum = sqrt(sqi[xi, xj] * sqj[xi, xj])
                sim[xi, xj] = prods[xi, xj] / denum

to

    for xi in range(n_x):
        sim[xi, xi] = 1
        for xj in range(xi + 1, n_x):
            if freq[xi, xj] < min_sprt:
                sim[xi, xj] = 0
            elif (sqi[xi, xj]==0) or (sqj[xi, xj]==0):
                raise ZeroDivisionError(
                   "Cosine similarity failed: at least one user (or item) has only zero "
                   "ratings. Rescale your ratings, or remove all-zero users/items."
               )
            else:
                denum = sqrt(sqi[xi, xj] * sqj[xi, xj])
                sim[xi, xj] = prods[xi, xj] / denum

Suggestion 2: set similarity to zero

Catch the case that the denominator is zero and set the similarity as equal to zero:

    for xi in range(n_x):
        sim[xi, xi] = 1
        for xj in range(xi + 1, n_x):
            if freq[xi, xj] < min_sprt:
                sim[xi, xj] = 0
            elif (sqi[xi, xj]==0) or (sqj[xi, xj]==0):
                sim[xi, xj] = 0
            else:
                denum = sqrt(sqi[xi, xj] * sqj[xi, xj])
                sim[xi, xj] = prods[xi, xj] / denum

that would be similar to scikit-learn's approach:

import sklearn.metrics
X = [[0, 0, 0]]
sklearn.metrics.pairwise.cosine_similarity(X, X)
# returns array([[0.]]), no error

Intuitively, if cosine is the criteria for similarity, then when that is undefined we interpret this as zero similarity. Maybe there should be a warning in the elif to make the user aware

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions