コード例 #1
0
ファイル: test_server.py プロジェクト: rtlee9/serveit
    def _setup(self, model, fit, data, predict=None, **kwargs):
        """Set up method to be called before each unit test.

        Arguments:
            - fit (callable): model training method; must accept args (data, target)
        """
        self.data = data
        fit(self.data.data, self.data.target)
        self.predict = predict or self.model.predict
        self.server_kwargs = kwargs
        self.server = ModelServer(self.model, self.predict, **kwargs)
        self.app = self.server.app.test_client()
コード例 #2
0
ファイル: test_server.py プロジェクト: rtlee9/serveit
    def test_postprocessing(self):
        """Test predictions endpoint with custom postprocessing callback."""
        # create test client with postprocessor that wraps predictions in a dictionary
        kwargs = self._update_kwargs_item(
            lambda x: dict(prediction=x.tolist()), 'postprocessor', 'last')
        server = ModelServer(self.model, self.predict, **kwargs)
        app = server.app.test_client()

        # generate sample data
        sample_data = self._get_sample_data()

        response = self._prediction_post(app, sample_data.tolist())
        response_data = json.loads(response.get_data())[
            'prediction']  # predictions are nested under 'prediction' key
        self.assertEqual(len(response_data), len(sample_data))
        if self.data.target.ndim > 1:
            # for multiclass each prediction should be one of the training labels
            for prediction in response_data:
                self.assertIn(prediction, self.data.target)
        else:
            # the average regression prediction for a sample of data should be similar
            # to the population mean
            # TODO: remove variance from this test (i.e., no chance of false negative)
            pred_pct_diff = np.array(
                response_data).mean() / self.data.target.mean() - 1
            self.assertAlmostEqual(pred_pct_diff / 1e4, 0, places=1)
コード例 #3
0
ファイル: test_server.py プロジェクト: rtlee9/serveit
    def test_preprocessing_list(self):
        """Test predictions endpoint with chained preprocessing callbacks."""
        # create test client with postprocessor that unraps data from a dict as the value of the 'data' key
        kwargs = self._update_kwargs_item(lambda d: d['data'], 'preprocessor')
        kwargs['preprocessor'] = [lambda d: d['data2']
                                  ] + kwargs['preprocessor']
        server = ModelServer(self.model, self.predict, **kwargs)
        app = server.app.test_client()

        # generate sample data, and wrap in dict keyed by 'data'
        sample_data = self._get_sample_data()
        data_dict = dict(data2=dict(data=sample_data.tolist()))

        response = self._prediction_post(app, data_dict)
        response_data = json.loads(response.get_data())
        self.assertEqual(len(response_data), len(sample_data))
        if self.data.target.ndim > 1:
            # for multiclass each prediction should be one of the training labels
            for prediction in response_data:
                self.assertIn(prediction, self.data.target)
        else:
            # the average regression prediction for a sample of data should be similar
            # to the population mean
            # TODO: remove variance from this test (i.e., no chance of false negative)
            pred_pct_diff = np.array(
                response_data).mean() / self.data.target.mean() - 1
            self.assertAlmostEqual(pred_pct_diff / 1e4, 0, places=1)
コード例 #4
0
ファイル: test_server.py プロジェクト: rtlee9/serveit
    def test_input_validation(self):
        """Add simple input validator and make sure it triggers."""

        # model input validator
        def feature_count_check(data):
            try:
                # convert PyTorch variables to numpy arrays
                data = data.data.numpy()
            except:
                pass
            # check num dims
            if data.ndim != 2:
                return False, 'Data should have two dimensions.'
            # check number of columns
            if data.shape[1] != self.data.data.shape[1]:
                reason = '{} features required, {} features provided'.format(
                    data.shape[1], self.data.data.shape[1])
                return False, reason
            # validation passed
            return True, None

        # set up test server
        server = ModelServer(self.model, self.predict, feature_count_check,
                             **self.server_kwargs)
        app = server.app.test_client()

        # generate sample data
        sample_data = self._get_sample_data()

        # post good data, verify 200 response
        response = self._prediction_post(app, sample_data.tolist())
        self.assertEqual(response.status_code, 200)

        # post bad data (drop a single column), verify 400 response
        response = self._prediction_post(app, sample_data[:, :-1].tolist())
        self.assertEqual(response.status_code, 400)
        response_data = json.loads(response.get_data())
        expected_reason = '{} features required, {} features provided'.format(
            self.data.data.shape[1] - 1, self.data.data.shape[1])
        self.assertIn(expected_reason, response_data['message'])
コード例 #5
0
ファイル: test_server.py プロジェクト: rtlee9/serveit
    def test_data_loader(self):
        """Test model prediction with a custom data loader callback."""

        # TODO: test alternative request method (e.g., URL params)
        # define custom data loader
        def read_json_from_dict():
            from flask import request
            # read data as the value of the 'data' key
            data = request.get_json()
            return np.array(data['data'])

        # create test client
        server = ModelServer(self.model,
                             self.predict,
                             data_loader=read_json_from_dict,
                             **self.server_kwargs)
        app = server.app.test_client()

        # generate sample data, and wrap in dict keyed by 'data'
        sample_data = self._get_sample_data()
        data_dict = dict(data=sample_data.tolist())

        response = self._prediction_post(app, data_dict)
        response_data = json.loads(response.get_data())
        self.assertEqual(len(response_data), len(sample_data))
        if self.data.target.ndim > 1:
            # for multiclass each prediction should be one of the training labels
            for prediction in response_data:
                self.assertIn(prediction, self.data.target)
        else:
            # the average regression prediction for a sample of data should be similar
            # to the population mean
            # TODO: remove variance from this test (i.e., no chance of false negative)
            pred_pct_diff = np.array(
                response_data).mean() / self.data.target.mean() - 1
            self.assertAlmostEqual(pred_pct_diff / 1e4, 0, places=1)
コード例 #6
0
ファイル: test_server.py プロジェクト: rtlee9/serveit
class ModelServerTest(object):
    """Base class to test the prediction server.

    ModelServerTest should be inherited by a class that has a `model` attribute,
    and calls `ModelServerTest._setup()` after instantiation. That class should
    also inherit from `unittest.TestCase` to ensure tests are executed.
    """
    def _setup(self, model, fit, data, predict=None, **kwargs):
        """Set up method to be called before each unit test.

        Arguments:
            - fit (callable): model training method; must accept args (data, target)
        """
        self.data = data
        fit(self.data.data, self.data.target)
        self.predict = predict or self.model.predict
        self.server_kwargs = kwargs
        self.server = ModelServer(self.model, self.predict, **kwargs)
        self.app = self.server.app.test_client()

    @staticmethod
    def _prediction_post(app, data):
        """Make a POST request to `app` with JSON body `data`."""
        return app.post(
            '/predictions',
            headers={'Content-Type': 'application/json'},
            data=json.dumps(data),
        )

    def _get_sample_data(self, n=100):
        """Return a sample of size n of self.data."""
        sample_idx = np.random.randint(self.data.data.shape[0], size=n)
        return self.data.data[sample_idx, :]

    def test_404_media(self):
        """Make sure API serves 404 response with JSON."""
        response = self.app.get('/fake-endpoint')
        self.assertEqual(response.status_code, 404)
        response_data_raw = response.get_data()
        self.assertIsNotNone(response_data_raw)
        response_data = json.loads(response_data_raw)
        self.assertGreater(len(response_data), 0)

    def test_features_info_none(self):
        """Verify 404 response if '/info/features' endpoint not yet created."""
        response = self.app.get('/info/features')
        self.assertEqual(response.status_code, 404)

    def test_features_info(self):
        """Test features info endpoint."""
        self.server.create_info_endpoint('features', self.data.feature_names)
        app = self.server.app.test_client()
        response = app.get('/info/features')
        response_data = json.loads(response.get_data())
        self.assertEqual(len(response_data), self.data.data.shape[1])
        try:
            self.assertCountEqual(response_data, self.data.feature_names)
        except AttributeError:  # Python 2
            self.assertItemsEqual(response_data, self.data.feature_names)

    def test_target_labels_info_none(self):
        """Verify 404 response if '/info/target_labels' endpoint not yet created."""
        response = self.app.get('/info/target_labels')
        self.assertEqual(response.status_code, 404)

    def test_target_labels_info(self):
        """Test target labels info endpoint."""
        if not hasattr(self.data, 'target_names'):
            return
        self.server.create_info_endpoint('target_labels',
                                         self.data.target_names.tolist())
        app = self.server.app.test_client()
        response = app.get('/info/target_labels')
        response_data = json.loads(response.get_data())
        self.assertEqual(len(response_data), self.data.target_names.shape[0])
        try:
            self.assertCountEqual(response_data, self.data.target_names)
        except AttributeError:  # Python 2
            self.assertItemsEqual(response_data, self.data.target_names)

    def test_predictions(self):
        """Test predictions endpoint."""
        sample_data = self._get_sample_data()
        response = self._prediction_post(self.app, sample_data.tolist())
        response_data = json.loads(response.get_data())
        self.assertEqual(len(response_data), len(sample_data))
        if self.data.target.ndim > 1:
            # for multiclass each prediction should be one of the training labels
            for prediction in response_data:
                self.assertIn(prediction, self.data.target)
        else:
            # the average regression prediction for a sample of data should be similar
            # to the population mean
            # TODO: remove variance from this test (i.e., no chance of false negative)
            pred_pct_diff = np.array(
                response_data).mean() / self.data.target.mean() - 1
            self.assertAlmostEqual(pred_pct_diff / 1e4, 0, places=1)

    def test_input_validation(self):
        """Add simple input validator and make sure it triggers."""

        # model input validator
        def feature_count_check(data):
            try:
                # convert PyTorch variables to numpy arrays
                data = data.data.numpy()
            except:
                pass
            # check num dims
            if data.ndim != 2:
                return False, 'Data should have two dimensions.'
            # check number of columns
            if data.shape[1] != self.data.data.shape[1]:
                reason = '{} features required, {} features provided'.format(
                    data.shape[1], self.data.data.shape[1])
                return False, reason
            # validation passed
            return True, None

        # set up test server
        server = ModelServer(self.model, self.predict, feature_count_check,
                             **self.server_kwargs)
        app = server.app.test_client()

        # generate sample data
        sample_data = self._get_sample_data()

        # post good data, verify 200 response
        response = self._prediction_post(app, sample_data.tolist())
        self.assertEqual(response.status_code, 200)

        # post bad data (drop a single column), verify 400 response
        response = self._prediction_post(app, sample_data[:, :-1].tolist())
        self.assertEqual(response.status_code, 400)
        response_data = json.loads(response.get_data())
        expected_reason = '{} features required, {} features provided'.format(
            self.data.data.shape[1] - 1, self.data.data.shape[1])
        self.assertIn(expected_reason, response_data['message'])

    def test_model_info(self):
        """Test model info endpoint."""
        response = self.app.get('/info/model')
        response_data = json.loads(response.get_data())
        self.assertGreater(len(response_data), 3)  # TODO: expand test scope

    def test_data_loader(self):
        """Test model prediction with a custom data loader callback."""

        # TODO: test alternative request method (e.g., URL params)
        # define custom data loader
        def read_json_from_dict():
            from flask import request
            # read data as the value of the 'data' key
            data = request.get_json()
            return np.array(data['data'])

        # create test client
        server = ModelServer(self.model,
                             self.predict,
                             data_loader=read_json_from_dict,
                             **self.server_kwargs)
        app = server.app.test_client()

        # generate sample data, and wrap in dict keyed by 'data'
        sample_data = self._get_sample_data()
        data_dict = dict(data=sample_data.tolist())

        response = self._prediction_post(app, data_dict)
        response_data = json.loads(response.get_data())
        self.assertEqual(len(response_data), len(sample_data))
        if self.data.target.ndim > 1:
            # for multiclass each prediction should be one of the training labels
            for prediction in response_data:
                self.assertIn(prediction, self.data.target)
        else:
            # the average regression prediction for a sample of data should be similar
            # to the population mean
            # TODO: remove variance from this test (i.e., no chance of false negative)
            pred_pct_diff = np.array(
                response_data).mean() / self.data.target.mean() - 1
            self.assertAlmostEqual(pred_pct_diff / 1e4, 0, places=1)

    def _update_kwargs_item(self, item, key_name, position='first'):
        """Prepend a method to the existing preprocessing chain, add to self's kwargs and return."""
        kwargs = self.server_kwargs
        if key_name in self.server_kwargs:
            existing_items = kwargs[key_name]
            if not isinstance(existing_items, (list, tuple)):
                existing_items = [existing_items]
        else:
            existing_items = []
        if position == 'first':
            kwargs[key_name] = [item] + existing_items
        if position == 'last':
            kwargs[key_name] = existing_items + [item]
        return kwargs

    def test_preprocessing(self):
        """Test predictions endpoint with custom preprocessing callback."""
        # create test client with postprocessor that unraps data from a dict as the value of the 'data' key
        kwargs = self._update_kwargs_item(lambda d: d['data'], 'preprocessor')
        server = ModelServer(self.model, self.predict, **kwargs)
        app = server.app.test_client()

        # generate sample data, and wrap in dict keyed by 'data'
        sample_data = self._get_sample_data()
        data_dict = dict(data=sample_data.tolist())

        response = self._prediction_post(app, data_dict)
        response_data = json.loads(response.get_data())
        self.assertEqual(len(response_data), len(sample_data))
        if self.data.target.ndim > 1:
            # for multiclass each prediction should be one of the training labels
            for prediction in response_data:
                self.assertIn(prediction, self.data.target)
        else:
            # the average regression prediction for a sample of data should be similar
            # to the population mean
            # TODO: remove variance from this test (i.e., no chance of false negative)
            pred_pct_diff = np.array(
                response_data).mean() / self.data.target.mean() - 1
            self.assertAlmostEqual(pred_pct_diff / 1e4, 0, places=1)

    def test_preprocessing_list(self):
        """Test predictions endpoint with chained preprocessing callbacks."""
        # create test client with postprocessor that unraps data from a dict as the value of the 'data' key
        kwargs = self._update_kwargs_item(lambda d: d['data'], 'preprocessor')
        kwargs['preprocessor'] = [lambda d: d['data2']
                                  ] + kwargs['preprocessor']
        server = ModelServer(self.model, self.predict, **kwargs)
        app = server.app.test_client()

        # generate sample data, and wrap in dict keyed by 'data'
        sample_data = self._get_sample_data()
        data_dict = dict(data2=dict(data=sample_data.tolist()))

        response = self._prediction_post(app, data_dict)
        response_data = json.loads(response.get_data())
        self.assertEqual(len(response_data), len(sample_data))
        if self.data.target.ndim > 1:
            # for multiclass each prediction should be one of the training labels
            for prediction in response_data:
                self.assertIn(prediction, self.data.target)
        else:
            # the average regression prediction for a sample of data should be similar
            # to the population mean
            # TODO: remove variance from this test (i.e., no chance of false negative)
            pred_pct_diff = np.array(
                response_data).mean() / self.data.target.mean() - 1
            self.assertAlmostEqual(pred_pct_diff / 1e4, 0, places=1)

    def test_postprocessing(self):
        """Test predictions endpoint with custom postprocessing callback."""
        # create test client with postprocessor that wraps predictions in a dictionary
        kwargs = self._update_kwargs_item(
            lambda x: dict(prediction=x.tolist()), 'postprocessor', 'last')
        server = ModelServer(self.model, self.predict, **kwargs)
        app = server.app.test_client()

        # generate sample data
        sample_data = self._get_sample_data()

        response = self._prediction_post(app, sample_data.tolist())
        response_data = json.loads(response.get_data())[
            'prediction']  # predictions are nested under 'prediction' key
        self.assertEqual(len(response_data), len(sample_data))
        if self.data.target.ndim > 1:
            # for multiclass each prediction should be one of the training labels
            for prediction in response_data:
                self.assertIn(prediction, self.data.target)
        else:
            # the average regression prediction for a sample of data should be similar
            # to the population mean
            # TODO: remove variance from this test (i.e., no chance of false negative)
            pred_pct_diff = np.array(
                response_data).mean() / self.data.target.mean() - 1
            self.assertAlmostEqual(pred_pct_diff / 1e4, 0, places=1)

    def test_get_app(self):
        """Make sure get_app method returns the same app."""
        self.assertEqual(self.server.get_app(), self.server.app)

    def test_400_no_content_type(self):
        """Check 400 response if no Content-Type header specified."""
        response = self.app.post('/predictions', )
        self.assertEqual(response.status_code, 400)
        response_body = json.loads(response.get_data())
        self.assertEqual(response_body['message'], 'Unable to fetch data')
        self.assertGreaterEqual(len(response_body['details']), 2)
コード例 #7
0

def validator(input_data):
    """Simple model input validator.

    Validator ensures the input data array is
        - two dimensional
        - has the correct number of features.
    """
    global data
    # check num dims
    if input_data.ndim != 2:
        return False, 'Data should have two dimensions.'
    # check number of columns
    if input_data.shape[1] != data.data.shape[1]:
        reason = '{} features required, {} features provided'.format(
            data.data.shape[1], input_data.shape[1])
        return False, reason
    # validation passed
    return True, None


# deploy model to a SkLearnServer
server = ModelServer(reg, reg.predict, validator)

# add informational endpoints
server.create_info_endpoint('features', data.feature_names)

# start API
server.serve()
コード例 #8
0
def validator(input_data):
    """Simple model input validator.

    Validator ensures the input data array is
        - two dimensional
        - has the correct number of features.
    """
    global data
    # check num dims
    if input_data.ndim != 2:
        return False, 'Data should have two dimensions.'
    # check number of columns
    if input_data.shape[1] != data.data.shape[1]:
        reason = '{} features required, {} features provided'.format(
            data.data.shape[1], input_data.shape[1])
        return False, reason
    # validation passed
    return True, None


# deploy model to a SkLearnServer
server = ModelServer(clf, clf.predict, validator)

# add informational endpoints
server.create_info_endpoint('features', data.feature_names)
server.create_info_endpoint('target_labels', data.target_names.tolist())

# start API
server.serve()
コード例 #9
0
# load DenseNet121 model pretrained on ImageNet
model = DenseNet121(weights='densenet121_weights_tf_dim_ordering_tf_kernels.h5')


# define a loader callback for the API to fetch the relevant data and
# preprocessor callbacks to map to a format expected by the model
def loader():
    """Load image from URL, and preprocess for densenet."""
    url = request.args.get('url')  # read image URL as a request URL param
    response = requests.get(url)  # make request to static image file
    return response.content

# get a bytes-to-image callback, resizing the image to 224x224 for ImageNet
bytes_to_image = get_bytes_to_image_callback(image_dims=(224, 224))

# deploy model to a ModelServer
server = ModelServer(
    model,
    model.predict,
    data_loader=loader,
    preprocessor=[bytes_to_image, preprocess_input],
    postprocessor=decode_predictions,
)

# get flask app for gunicorn serving
app = server.get_app()

if __name__ == '__main__':
    server.serve()
コード例 #10
0
    samples = gensamples(
        skips=2,
        k=1,
        batch_size=2,
        short=False,
        temperature=1.,
        use_unk=True,
        model=model,
        data=(x, y),
        idx2word=idx2word,
        oov0=oov0,
        glove_idx2idx=glove_idx2idx,
        vocab_size=vocab_size,
        nb_unknown_words=nb_unknown_words,
    )

    headline = samples[0][0][len(samples[0][1]):]
    return ' '.join(idx2word[w] for w in headline)


server = ModelServer(
    model,
    predict,
    data_loader=loader,
    to_numpy=False,
)

# start API
server.serve()
コード例 #11
0
    return response.content

#  define preprocessing callback chain
preprocessor = [
    get_bytes_to_image_callback(image_dims=(224, 224)),  # convert bytes to image of size 224 x 224
    lambda img: torch.from_numpy(img.swapaxes(3, 1).swapaxes(2, 3).copy()) / 255,  # convert to tensor, rescale
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),  # normalize pixel intensities
    torch.autograd.Variable,  # convert to PyTorch Variable
]


# define a postprocessor callback for the API to transform the model predictions
def postprocessor(prediction):
    """Map prediction tensor to labels."""
    prediction = prediction.data.numpy()[0]
    top_predictions = prediction.argsort()[-3:][::-1]
    return [labels[prediction] for prediction in top_predictions]

# deploy model to a ModelServer
server = ModelServer(
    model,
    model,
    data_loader=loader,
    preprocessor=preprocessor,
    postprocessor=postprocessor,
    to_numpy=False
)

# start API
server.serve()