| 11 | |
| 12 | |
| 13 | class ActionTokenizer: |
| 14 | def __init__( |
| 15 | self, tokenizer: PreTrainedTokenizerBase, bins: int = 256, min_action: int = -1, max_action: int = 1 |
| 16 | ) -> None: |
| 17 | """ |
| 18 | Discretizes continuous robot actions into N bins per dimension and maps to the least used tokens. |
| 19 | |
| 20 | NOTE =>> by default, assumes a BPE-style tokenizer akin to the LlamaTokenizer, where *the least used tokens* |
| 21 | appear at the end of the vocabulary! |
| 22 | |
| 23 | :param tokenizer: Base LLM/VLM tokenizer to extend. |
| 24 | :param bins: Number of bins for each continuous value; we'll adopt a uniform binning strategy. |
| 25 | :param min_action: Minimum action value (for clipping, setting lower bound on bin interval). |
| 26 | :param max_action: Maximum action value (for clipping, setting upper bound on bin interval). |
| 27 | """ |
| 28 | self.tokenizer, self.n_bins, self.min_action, self.max_action = tokenizer, bins, min_action, max_action |
| 29 | |
| 30 | # Create Uniform Bins + Compute Bin Centers |
| 31 | self.bins = np.linspace(min_action, max_action, self.n_bins) |
| 32 | self.bin_centers = (self.bins[:-1] + self.bins[1:]) / 2.0 |
| 33 | |
| 34 | # [Contract] Set "action_token_begin_idx" based on `self.tokenizer.vocab_size - (self.n_bins + 1)` |
| 35 | # =>> Assumes we're always overwriting the final `n_bins` tokens of the vocabulary! |
| 36 | self.action_token_begin_idx: int = int(self.tokenizer.vocab_size - (self.n_bins + 1)) |
| 37 | |
| 38 | def __call__(self, action: np.ndarray) -> Union[str, List[str]]: |
| 39 | """Clip & bin actions to *the last `n_bins` tokens* of the vocabulary (e.g., tokenizer.vocab[-256:]).""" |
| 40 | action = np.clip(action, a_min=float(self.min_action), a_max=float(self.max_action)) |
| 41 | discretized_action = np.digitize(action, self.bins) |
| 42 | |
| 43 | # Handle single element vs. batch |
| 44 | if len(discretized_action.shape) == 1: |
| 45 | return self.tokenizer.decode(list(self.tokenizer.vocab_size - discretized_action)) |
| 46 | else: |
| 47 | return self.tokenizer.batch_decode((self.tokenizer.vocab_size - discretized_action).tolist()) |
| 48 | |
| 49 | def decode_token_ids_to_actions(self, action_token_ids: np.ndarray) -> np.ndarray: |
| 50 | """ |
| 51 | Returns continuous actions for discrete action token IDs. |
| 52 | |
| 53 | NOTE =>> Because of the way the actions are discretized w.r.t. the bins (and not the bin centers), the |
| 54 | digitization returns bin indices between [1, # bins], inclusive, when there are actually only |
| 55 | (# bins - 1) bin intervals. |
| 56 | |
| 57 | Therefore, if the digitization returns the last possible index, we map this to the last bin interval. |
| 58 | |
| 59 | EXAMPLE =>> Let's say self._bins has 256 values. Then self._bin_centers has 255 values. Digitization returns |
| 60 | indices between [1, 256]. We subtract 1 from all indices so that they are between [0, 255]. There |
| 61 | is still one index (i==255) that would cause an out-of-bounds error if used to index into |
| 62 | self._bin_centers. Therefore, if i==255, we subtract 1 from it so that it just becomes the index of |
| 63 | the last bin center. We implement this simply via clipping between [0, 255 - 1]. |
| 64 | """ |
| 65 | discretized_actions = self.tokenizer.vocab_size - action_token_ids |
| 66 | discretized_actions = np.clip(discretized_actions - 1, a_min=0, a_max=self.bin_centers.shape[0] - 1) |
| 67 | |
| 68 | return self.bin_centers[discretized_actions] |
| 69 | |
| 70 | @property |
no outgoing calls
no test coverage detected