docstr: Fits the LightGBM model.
  
  :param train_data: encoded features for training dataset
  :param dev_data: encoded features for dev dataset
docstr_tok: ["Fits","the","LightGBM","model",".",":","param","train_data",":","encoded","features","for","training","dataset",":","param","dev_data",":","encoded","features","for","dev","dataset"]
code: def fit(self, train_data: EncodedDs, dev_data: EncodedDs) -> None:
      """
      Fits the LightGBM model.
  
      :param train_data: encoded features for training dataset
      :param dev_data: encoded features for dev dataset
      """
      started = time.time()
  
      log.info("Started fitting LGBM model")
      data = {
          "train": {"ds": train_data, "data": None, "label_data": {}},
          "dev": {"ds": dev_data, "data": None, "label_data": {}},
      }
      self.fit_data_len = len(data["train"]["ds"])
      self.positive_domain = getattr(
          train_data.encoders.get(self.target, None),
          "positive_domain",
          False,
      )
  
      output_dtype = self.dtype_dict[self.target]
      data = self._to_dataset(data, output_dtype)
  
      if output_dtype not in self.cls_dtypes + self.num_dtypes:
          log.error(
              f"Lightgbm mixer not supported for type: {output_dtype}"
          )
          raise Exception(
              f"Lightgbm mixer not supported for type: {output_dtype}"
          )
      else:
          objective = (
              "regression"
              if output_dtype in self.num_dtypes
              else "multiclass"
          )
          metric = (
              "l2"
              if output_dtype in self.num_dtypes
              else "multi_logloss"
          )
  
      self.params = {
          "objective": objective,
          "metric": metric,
          "verbose": -1,
          "lambda_l1": 0.1,
          "lambda_l2": 0.1,
          "force_row_wise": True,
          "device_type": self.device_str,
      }
  
      if objective == "multiclass":
          self.all_classes = self.ordinal_encoder.categories_[0]
          self.params["num_class"] = self.all_classes.size
      elif output_dtype == dtype.num_tsarray:
          self.params["linear_tree"] = True
      if self.device_str == "gpu":
          self.params["gpu_use_dp"] = True
  
      # Determine time per iterations
      start = time.time()
      self.params["num_iterations"] = 1
      """
          Why construst a dataset here instead of using the training dataset?
  
          Because it guards against the following crash:
  
          WARNING:lightwood-1613058:Exception: Cannot change feature_pre_filter after constructed Dataset handle. when training mixer: <lightwood.mixer.lightgbm.LightGBM object at 0x7f39148eae20>
          feature_fraction, val_score: inf:   0%|                                 | 0/7 [00:00<?, ?it/s]
          free(): double free detected in tcache 2
  
          Only happens sometimes and I can find no pattern as to when, happens for multiple input and target types.
  
          Why does the following crash happen and what does it mean? No idea, closest relationships I can find is /w optuna modifying parameters after the dataset is create: https://github.com/microsoft/LightGBM/issues/4019 | But why this would apply here makes no sense. Could have to do with the `train` process of lightgbm itself setting a "set only once" property on a dataset when it starts. Dunno, if you find out replace this comment with the real reason.
          """  # noqa
      kwargs = {}
      if "verbose_eval" in inspect.getfullargspec(lightgbm.train).args:
          kwargs["verbose_eval"] = False
      self.model = lightgbm.train(
          self.params,
          lightgbm.Dataset(
              data["train"]["data"],
              label=data["train"]["label_data"],
              weight=data["train"]["weights"],
          ),
          **kwargs,
      )
      end = time.time()
      seconds_for_one_iteration = max(0.1, end - start)
  
      self.stop_after = max(
          1, self.stop_after - (time.time() - started)
      )
      # Determine nr of iterations
      log.info(
          f"A single GBM iteration takes {seconds_for_one_iteration} seconds"
      )
      self.num_iterations = int(
          self.stop_after * 0.8 / seconds_for_one_iteration
      )
  
      # Turn on grid search if training doesn't take too long using it
      kwargs = {}
      if self.use_optuna and self.num_iterations >= 200:
          model_generator = optuna_lightgbm
          kwargs["time_budget"] = self.stop_after * 0.4
          self.num_iterations = int(self.num_iterations / 2)
          kwargs["optuna_seed"] = 0
      else:
          model_generator = lightgbm
  
      # Train the models
      log.info(
          f"Training GBM ({model_generator}) with {self.num_iterations} iterations given {self.stop_after} seconds constraint"
      )  # noqa
      if self.num_iterations < 1:
          self.num_iterations = 1
      self.params["num_iterations"] = int(self.num_iterations)
  
      self.params["early_stopping_rounds"] = 5
  
      # Prepare the data
      train_dataset = lightgbm.Dataset(
          data["train"]["data"],
          label=data["train"]["label_data"],
          weight=data["train"]["weights"],
      )
  
      dev_dataset = lightgbm.Dataset(
          data["dev"]["data"],
          label=data["dev"]["label_data"],
          weight=data["dev"]["weights"],
      )
  
      if "verbose_eval" in inspect.getfullargspec(lightgbm.train).args:
          kwargs["verbose_eval"] = False
      self.model = model_generator.train(
          self.params,
          train_dataset,
          valid_sets=[dev_dataset, train_dataset],
          valid_names=["dev", "train"],
          **kwargs,
      )
      self.num_iterations = self.model.best_iteration
      log.info(
          f"Lightgbm model contains {self.model.num_trees()} weak estimators"
      )
  
      if self.fit_on_dev:
          self.partial_fit(dev_data, train_data)
  
imports: import time
  import inspect
  from typing import Dict, List, Set, Optional
  import torch
  import optuna
  import lightgbm
  import numpy
  import pandas
  from sklearn.preprocessing import OrdinalEncoder
  import optuna.integration.lightgbm
  from type_infer.dtype import dtype
  from lightwood.encoder.base import BaseEncoder
  from lightwood.helpers.log import log
  from lightwood.mixer.base import BaseMixer
  from lightwood.helpers.device import get_devices
  from lightwood.api.types import PredictionArguments
  from lightwood.data.encoded_ds import EncodedDs
label_desc: Use image node_id 3 for calling the LightGBM obj's underlying member method code with example usage: obj.fit(train_data, dev_data) without return types