PyTorch loss function referencing model parameters
For an assignment, I need to create a movie recommender system that uses a supplied loss function:

sum(i=1,M) sum(j=1,M) indicator[i̸=j](viT vj − Xi,j )**2



What this means is, the dot product between two movie embeddings, Vi and Vj should be pretty darn close to Xi,j. Where Xi,j is the sum of users who liked both movie i and movie j. Entries where i == j are omitted by the indicator function, (set to 0.)

The assignment deliverable is a weight matrix from the hidden layer. Its dimensions should be 9724x300, where there are 9724 unique movie IDs, and 300 neurons. 300 is an arbitrary choice, influenced by use of 300 neurons in Google's word2vec.

What I have: 


source_data: rows are users, columns are movies. A 1 in a given cell means that the corresponding user liked the corresponding movie (dislike = 0.)
preprocessed_data: matrix multiplication of source_data transposed on itself. (i.e. every cell is the sum of users who liked BOTH movie i and j. The diagonal entries are not of use to me, as they're just the sum of users who liked a single movie.)


Where I'm stuck:


Not sure how to define my own loss function in terms of i and j that can compare the sum of users who like movies i and j in preprocessed_data to rows, i and j, in hidden layer weight tensor.
Not sure how to organize my data into X and y tensors, such that the data is appropriate to the loss function.


Before you read further, please note that asking for and receiving direct help in your assignment from StackOverflow may be against the rules of your school and lead to consequences for you as a student!

That being said, the way I would model this problem is as follows:

import torch

U = 300 # number of users
M = 30  # number of movies
D = 4   # dimension of embedding vectors

source = torch.randint(0, 2, (U, M)) # users' ratings
X = source.transpose(0, 1) @ source  # your `preprocessed_data`

# initial values for your embedding. This is what your algorithm needs to learn
v = torch.randn(M, D, requires_grad=True)
X = X.to(torch.float32) # necessary to be in line with `v`

# this is the `(viT vj − Xi,j )**2` part
loss_elementwise = (v @ v.transpose(0, 1) - X).pow(2)

# now we need to get rid of the diagonal. Notice that we can equally
# well get rid of the diagonal and the whole upper triangular part,
# as well, since both V @ V.T and source.T @ source are symmetric, so
# the upper triangular part contains just
# a mirror reflection of the lower triangular part.
# This means that we actually implement a bit different summation:
# sum(i=1,M) sum(j=1,i-1) stuff(i, j)
# instead of
# sum(i=1,M) sum(j=1,M) indicator[i̸=j] stuff(i, j)
# and get exactly half the original value
masked = torch.tril(loss_elementwise, -1)

# finally we sum it up, multiplying by 2 to make up
# for the "lost" upper triangular part
loss = 2 * masked.sum()


Now what is left for you to implement is the optimization loop which will use gradient of loss to optimize the values of v.
