-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Basic implementation of RankNet. Added RankNetLoss into NeuralNetwork…
…'s component.
- Loading branch information
Showing
6 changed files
with
263 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import unittest | ||
|
||
from vanilla_ml.supervised.ranking.rank_net import RankNet | ||
from vanilla_ml.util import data_io | ||
from vanilla_ml.util.metrics.ranking import ndcg | ||
|
||
|
||
class TestRankNet(unittest.TestCase): | ||
|
||
def test_iris_two_classes(self): | ||
train_X, test_X, train_y, test_y = data_io.get_ranking_train_test() | ||
print("train_X's shape = %s, train_y's shape = %s" % (train_X.shape, train_y.shape)) | ||
print("test_X's shape = %s, test_y's shape = %s" % (test_X.shape, test_y.shape)) | ||
|
||
layers = [100] | ||
rnk = RankNet(layers, batch_size=train_X.shape[0], n_epochs=10, learning_rate=0.1) | ||
print("rnk: %s" % rnk) | ||
|
||
print("Fitting ...") | ||
rnk.fit(train_X, train_y) | ||
|
||
print("Predicting ...") | ||
pred_proba_y = rnk.rank_score(test_X) | ||
pred_y = rnk.rank(test_X) | ||
print("y = %s" % test_y) | ||
print("pred_proba_y = %s" % pred_proba_y) | ||
print("pred_y = %s" % pred_y) | ||
|
||
k = 5 | ||
ndcg_score = ndcg(test_y, pred_proba_y, k) | ||
print("NDCG@%d = %g" % (k, ndcg_score)) | ||
|
||
self.assertGreaterEqual(ndcg_score, 0.91) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
from abc import ABCMeta, abstractmethod | ||
|
||
|
||
class AbstractRanker(object): | ||
""" | ||
Abstract ranker | ||
""" | ||
__metaclass__ = ABCMeta | ||
|
||
@abstractmethod | ||
def fit(self, X, y, sample_weights=None): | ||
""" Fit the model using the given training data set with n data points and p features. | ||
Args: | ||
X (ndarray): training data set, shape N x P. | ||
y (ndarray): training ranks, shape N x 1. | ||
sample_weights (Optional[ndarray]): sample weights, shape N x 1. | ||
""" | ||
pass | ||
|
||
@abstractmethod | ||
def rank_score(self, X): | ||
""" Compute ranking scores for the test set. | ||
Args: | ||
X (ndarray): test set, shape M x P. | ||
Returns: | ||
ndarray: ranking scores, shape N. | ||
""" | ||
pass | ||
|
||
def rank(self, X): | ||
""" Rank elements from the test set. The elements are sorted in descending | ||
order of ranking scores. | ||
Args: | ||
X (ndarray): test set, shape M x P. | ||
Returns: | ||
ndarray: ranked element's indices, shape N. | ||
""" | ||
scores = self.rank_score(X).ravel() | ||
return scores.argsort()[::-1] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
""" | ||
RankNet using Feed-forward Neural Network. | ||
1) "From RankNet to LambdaRank to LambdaMART: An Overview", Christ Burges. | ||
2) "Learning to Rank using Gradient Descent", Chris Burges et. al. | ||
""" | ||
import numpy as np | ||
|
||
from vanilla_ml.base.neural_network.activators import Sigmoid | ||
from vanilla_ml.base.neural_network.containers import Sequential | ||
from vanilla_ml.base.neural_network.layers import Linear | ||
from vanilla_ml.base.neural_network.loss import RankNetLoss | ||
from vanilla_ml.supervised.ranking.abstract_ranker import AbstractRanker | ||
from vanilla_ml.util.metrics.ranking import ndcg | ||
|
||
|
||
class RankNet(AbstractRanker): | ||
|
||
def __init__(self, layers, learning_rate=1.0, batch_size=10, | ||
n_epochs=50, tol=1e-5, verbose=True, random_state=42): | ||
|
||
assert learning_rate > 0, "Learning rate must be positive." | ||
|
||
self.layers = layers | ||
self.lr = learning_rate | ||
self.batch_size = batch_size | ||
self.n_epochs = n_epochs | ||
self.tol = tol | ||
self.verbose = verbose | ||
self.random_state = random_state | ||
self.input_size = None | ||
self.model = None | ||
self.loss = None | ||
|
||
def fit(self, X, y, sample_weights=None): | ||
assert sample_weights is None, "Specifying sample weights is not supported!" | ||
assert len(X) == len(y), "Length mismatches: len(X) = %d, len(y) = %d" % (len(X), len(y)) | ||
|
||
np.random.seed(self.random_state) | ||
n_samples, self.input_size = X.shape | ||
|
||
# Model | ||
self.model, self.loss = self._build_model() | ||
|
||
# SGD params | ||
params = {"lrate": self.lr, "max_grad_norm": 40} | ||
|
||
indices = np.arange(n_samples) | ||
|
||
# Run SGD | ||
for epoch in range(self.n_epochs): | ||
if self.verbose and (epoch + 1) % 10 == 0: | ||
print("\n * Epoch %d ..." % (epoch + 1)) | ||
|
||
# For report | ||
# total_ndcg_score = 0. | ||
# total_cost = 0. | ||
# total_num = 0 | ||
|
||
for it in range(n_samples / self.batch_size): | ||
|
||
# batch = np.random.choice(indices, size=self.batch_size, replace=False) | ||
start = it * self.batch_size | ||
end = min((it + 1) * self.batch_size, n_samples) | ||
batch = indices[start:end] | ||
input_data, target_data = X[batch], y[batch] | ||
|
||
# Forward propagation | ||
pred = self.model.fprop(input_data) | ||
# total_cost += self.loss.fprop(pred, target_data) | ||
# total_num += self.batch_size | ||
ndcg_score = ndcg(target_data, pred, k=10) | ||
# total_ndcg_score += ndcg_score | ||
|
||
if self.verbose: | ||
print("\n* Iter %d" % (it + 1)) | ||
print("Train NDCG@10: %g" % ndcg_score) | ||
|
||
# Backward propagation | ||
grad_output = self.loss.bprop(pred, target_data) | ||
self.model.bprop(input_data, grad_output) | ||
self.model.update(params) | ||
|
||
def rank_score(self, X): | ||
return self.model.fprop(X).ravel() | ||
|
||
def _build_model(self): | ||
input_size, layer_sizes = self.input_size, self.layers | ||
model = Sequential() | ||
for i in range(len(layer_sizes)): | ||
if i == 0: | ||
model.add(Linear(input_size, layer_sizes[i])) | ||
else: | ||
model.add(Linear(layer_sizes[i - 1], layer_sizes[i])) | ||
model.add(Sigmoid()) | ||
# model.add(ReLU()) | ||
|
||
model.add(Linear(layer_sizes[-1], 1)) | ||
|
||
# Cost | ||
loss = RankNetLoss(sigma=1, size_average=True) | ||
|
||
return model, loss |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters