mirror of
https://github.com/facebookresearch/ReAgent.git
synced 2026-06-16 12:44:41 +00:00
Summary: Pull Request resolved: https://github.com/facebookresearch/ReAgent/pull/348 Test Plan: This code passes flow-cli canary rl.workflow.test.test_open_ai_gym_offline.test_gym_discrete_crr@reinforcement_learning --parameters-file=fblearner/flow/projects/rl/configs/gym_offline/test_gym_cartpole_crr.json --mode opt --entitlement=gpu_prod --run-as-secure-group reinforcement_learning --force-build Reviewed By: kittipatv Differential Revision: D25321153 Pulled By: DavidV17 fbshipit-source-id: f45c0763f4554dedee5a5f299141043adb679572
243 lines
7.9 KiB
Python
243 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
|
|
|
|
import collections
|
|
import csv
|
|
|
|
import numpy as np
|
|
import six
|
|
from reagent.preprocessing import identify_types
|
|
from reagent.preprocessing.normalization import (
|
|
BOX_COX_MARGIN,
|
|
MAX_FEATURE_VALUE,
|
|
MIN_FEATURE_VALUE,
|
|
MISSING_VALUE,
|
|
NormalizationParameters,
|
|
)
|
|
from scipy import special, stats
|
|
|
|
|
|
def default_normalizer(feats, min_value=None, max_value=None):
|
|
normalization_types = [
|
|
NormalizationParameters(
|
|
feature_type="BINARY",
|
|
boxcox_lambda=None,
|
|
boxcox_shift=None,
|
|
mean=0,
|
|
stddev=1,
|
|
possible_values=None,
|
|
quantiles=None,
|
|
min_value=min_value,
|
|
max_value=max_value,
|
|
),
|
|
NormalizationParameters(
|
|
feature_type="PROBABILITY",
|
|
boxcox_lambda=None,
|
|
boxcox_shift=None,
|
|
mean=0,
|
|
stddev=1,
|
|
possible_values=None,
|
|
quantiles=None,
|
|
min_value=min_value,
|
|
max_value=max_value,
|
|
),
|
|
NormalizationParameters(
|
|
feature_type="CONTINUOUS",
|
|
boxcox_lambda=None,
|
|
boxcox_shift=None,
|
|
mean=0,
|
|
stddev=1,
|
|
possible_values=None,
|
|
quantiles=None,
|
|
min_value=min_value,
|
|
max_value=max_value,
|
|
),
|
|
NormalizationParameters(
|
|
feature_type="BOXCOX",
|
|
boxcox_lambda=1,
|
|
boxcox_shift=1,
|
|
mean=0,
|
|
stddev=1,
|
|
possible_values=None,
|
|
quantiles=None,
|
|
min_value=min_value,
|
|
max_value=max_value,
|
|
),
|
|
NormalizationParameters(
|
|
feature_type="QUANTILE",
|
|
boxcox_lambda=None,
|
|
boxcox_shift=None,
|
|
mean=0,
|
|
stddev=1,
|
|
possible_values=None,
|
|
quantiles=[0, 1],
|
|
min_value=min_value,
|
|
max_value=max_value,
|
|
),
|
|
NormalizationParameters(
|
|
feature_type="ENUM",
|
|
boxcox_lambda=None,
|
|
boxcox_shift=None,
|
|
mean=0,
|
|
stddev=1,
|
|
possible_values=[0, 1],
|
|
quantiles=None,
|
|
min_value=min_value,
|
|
max_value=max_value,
|
|
),
|
|
]
|
|
normalization = collections.OrderedDict(
|
|
[
|
|
(feats[i], normalization_types[i % len(normalization_types)])
|
|
for i in range(len(feats))
|
|
]
|
|
)
|
|
return normalization
|
|
|
|
|
|
def normalizer_helper(feats, feature_type, min_value=None, max_value=None):
|
|
assert feature_type in (
|
|
"DISCRETE_ACTION",
|
|
"CONTINUOUS",
|
|
"CONTINUOUS_ACTION",
|
|
), f"invalid feature type: {feature_type}."
|
|
assert type(min_value) == type(max_value) and type(min_value) in (
|
|
int,
|
|
float,
|
|
list,
|
|
np.ndarray,
|
|
type(None),
|
|
), f"invalid {type(min_value)}, {type(max_value)}"
|
|
if type(min_value) in [int, float, type(None)]:
|
|
min_value = [min_value] * len(feats)
|
|
max_value = [max_value] * len(feats)
|
|
normalization = collections.OrderedDict(
|
|
[
|
|
(
|
|
feats[i],
|
|
NormalizationParameters(
|
|
feature_type=feature_type,
|
|
boxcox_lambda=None,
|
|
boxcox_shift=None,
|
|
mean=0,
|
|
stddev=1,
|
|
possible_values=None,
|
|
quantiles=None,
|
|
min_value=float(min_value[i]) if min_value[i] is not None else None,
|
|
max_value=float(max_value[i]) if max_value[i] is not None else None,
|
|
),
|
|
)
|
|
for i in range(len(feats))
|
|
]
|
|
)
|
|
return normalization
|
|
|
|
|
|
def discrete_action_normalizer(feats):
|
|
return normalizer_helper(feats, "DISCRETE_ACTION")
|
|
|
|
|
|
def only_continuous_normalizer(feats, min_value=None, max_value=None):
|
|
return normalizer_helper(feats, "CONTINUOUS", min_value, max_value)
|
|
|
|
|
|
def only_continuous_action_normalizer(feats, min_value=None, max_value=None):
|
|
return normalizer_helper(feats, "CONTINUOUS_ACTION", min_value, max_value)
|
|
|
|
|
|
def write_lists_to_csv(path, *args):
|
|
rows = zip(*args)
|
|
with open(path, "w") as f:
|
|
writer = csv.writer(f)
|
|
for row in rows:
|
|
writer.writerow(row)
|
|
|
|
|
|
class NumpyFeatureProcessor(object):
|
|
@staticmethod
|
|
def value_to_quantile(original_value, quantiles):
|
|
if original_value <= quantiles[0]:
|
|
return 0.0
|
|
if original_value >= quantiles[-1]:
|
|
return 1.0
|
|
n_quantiles = float(len(quantiles) - 1)
|
|
right = np.searchsorted(quantiles, original_value)
|
|
left = right - 1
|
|
interpolated = (
|
|
left
|
|
+ (
|
|
(original_value - quantiles[left])
|
|
/ ((quantiles[right] + 1e-6) - quantiles[left])
|
|
)
|
|
) / n_quantiles
|
|
return interpolated
|
|
|
|
@classmethod
|
|
def preprocess_feature(cls, feature, parameters):
|
|
is_not_empty = 1 - np.isclose(feature, MISSING_VALUE)
|
|
if parameters.feature_type == identify_types.BINARY:
|
|
# Binary features are always 1 unless they are 0
|
|
return ((feature != 0) * is_not_empty).astype(np.float32)
|
|
if parameters.boxcox_lambda is not None:
|
|
feature = stats.boxcox(
|
|
np.maximum(feature + parameters.boxcox_shift, BOX_COX_MARGIN),
|
|
parameters.boxcox_lambda,
|
|
)
|
|
# No *= to ensure consistent out-of-place operation.
|
|
if parameters.feature_type == identify_types.PROBABILITY:
|
|
feature = np.clip(feature, 0.01, 0.99)
|
|
feature = special.logit(feature)
|
|
elif parameters.feature_type == identify_types.QUANTILE:
|
|
transformed_feature = np.zeros_like(feature)
|
|
for i in six.moves.range(feature.shape[0]):
|
|
transformed_feature[i] = cls.value_to_quantile(
|
|
feature[i], parameters.quantiles
|
|
)
|
|
feature = transformed_feature
|
|
elif parameters.feature_type == identify_types.ENUM:
|
|
possible_values = parameters.possible_values
|
|
mapping = {}
|
|
for i, possible_value in enumerate(possible_values):
|
|
mapping[possible_value] = i
|
|
output_feature = np.zeros((len(feature), len(possible_values)))
|
|
for i, val in enumerate(feature):
|
|
if abs(val - MISSING_VALUE) < 1e-2:
|
|
# This check is required by the PT preprocessing but not C2
|
|
continue
|
|
output_feature[i][mapping[val]] = 1.0
|
|
return output_feature
|
|
elif parameters.feature_type == identify_types.CONTINUOUS_ACTION:
|
|
min_value = parameters.min_value
|
|
max_value = parameters.max_value
|
|
feature = (
|
|
(feature - min_value) * ((1 - 1e-6) * 2 / (max_value - min_value))
|
|
- 1
|
|
+ 1e-6
|
|
)
|
|
else:
|
|
feature = feature - parameters.mean
|
|
feature /= parameters.stddev
|
|
feature = np.clip(feature, MIN_FEATURE_VALUE, MAX_FEATURE_VALUE)
|
|
feature *= is_not_empty
|
|
return feature
|
|
|
|
@classmethod
|
|
def preprocess(cls, features, parameters):
|
|
result = {}
|
|
for feature_name in features:
|
|
result[feature_name] = cls.preprocess_feature(
|
|
features[feature_name], parameters[feature_name]
|
|
)
|
|
return result
|
|
|
|
@classmethod
|
|
def preprocess_array(cls, arr, features, parameters):
|
|
assert len(arr.shape) == 2 and arr.shape[1] == len(features)
|
|
preprocessed_values = [
|
|
cls.preprocess({f: v for f, v in zip(features, row)}, parameters)
|
|
for row in arr
|
|
]
|
|
return np.array(
|
|
[[ex[f] for f in features] for ex in preprocessed_values], dtype=np.float32
|
|
)
|