lib/smartcell/ml_svm.ex

if !Code.ensure_loaded?(Kino.SmartCell) do
  defmodule Evision.SmartCell.ML.SVM do
  end
else
  defmodule Evision.SmartCell.ML.SVM do
    use Kino.JS, assets_path: "lib/assets"
    use Kino.JS.Live
    use Kino.SmartCell, name: "Evision: Support Vector Machine"

    alias Evision.SmartCell.Helper, as: ESCH
    alias Evision.SmartCell.ML.TrainData

    @smartcell_id "evision.ml.svm"

    @properties %{
      "data_from" => %{
        :type => :string,
        :opts => [must_in: ["traindata_var", "traindata"]],
        :default => "traindata_var"
      },
      "traindata_var" => %{
        :type => :string,
        :default => "dataset"
      },

      # SVM
      "type" => %{
        :type => :string,
        :opts => [must_in: ["C_SVC", "NU_SVC", "ONE_CLASS", "EPS_SVR", "NU_SVR"]],
        :default => "C_SVC"
      },
      "kernel_type" => %{
        :type => :string,
        :opts => [must_in: ["LINEAR", "POLY", "RBF", "SIGMOID", "CHI2", "INTER", "CUSTOM"]],
        :default => "RBF"
      },
      "to_variable" => %{
        :type => :string,
        :default => "svm"
      },

      ## -- kernel parameter --
      ## Only used for SVM if its kernel type is one of
      ## [SVM::POLY, SVM::RBF, SVM::SIGMOID, SVM::CHI2]
      "gamma" => %{
        :type => :number,
        :default => 1
      },
      ## Only used for SVM if its kernel type is one of
      ## [SVM::POLY, SVM::SIGMOID]
      "coef0" => %{
        :type => :number,
        :default => 0
      },
      ## Only used for SVM if its kernel type is one of
      ## [SVM::POLY]
      "degree" => %{
        :type => :number,
        :default => 0
      },

      ## -- svm parameter --
      ## Only used for SVM if its type is one of
      ## [SVM::C_SVC, SVM::EPS_SVR, SVM::NU_SVR]
      "c" => %{
        :type => :number,
        :default => 1
      },
      ## Only used for SVM if its type is one of
      ## [SVM::NU_SVC, SVM::ONE_CLASS or SVM::NU_SVR]
      "nu" => %{
        :type => :number,
        :default => 0
      },
      ## Only used for SVM if its type is one of
      ## [SVM::EPS_SVR]
      "p" => %{
        :type => :number,
        :default => 0
      },

      # TermCriteria
      "term_criteria_type" => %{
        :type => :string,
        :opts => [must_in: ["max_count", "eps", "max_count+eps"]],
        :default => "max_count"
      },
      "term_criteria_count" => %{
        :type => :integer,
        :opts => [minimum: 0],
        :default => 10
      },
      "term_criteria_eps" => %{
        :type => :number,
        :default => 0
      },
    }
    @inner_to_module %{
      "traindata" => TrainData
    }

    @spec id :: String.t()
    def id, do: @smartcell_id

    @spec properties :: map()
    def properties, do: @properties

    @spec defaults :: map()
    def defaults do
      Map.new(Enum.map(@properties, fn {field, field_specs} ->
        {field, field_specs[:default]}
      end))
    end

    @impl true
    def init(attrs, ctx) do
      # load from file or fill empty entries with default values
      fields =
        Map.new(Enum.map(@properties, fn {field, field_specs} ->
          {field, attrs[field] || field_specs[:default]}
        end))

      # traindata
      key = "traindata"
      fields = ESCH.update_key_with_module(fields, key, @inner_to_module[key], fn fields, key ->
        fields["data_from"] == key
      end)

      info = [id: @smartcell_id, fields: fields]
      {:ok, assign(ctx, info)}
    end

    @impl true
    def handle_connect(ctx) do
      {:ok, %{id: ctx.assigns.id, fields: ctx.assigns.fields}, ctx}
    end

    @impl true
    def handle_event("update_field", %{"field" => field, "value" => value}, ctx) do
      updated_fields =
        case String.split(field, ".", parts: 2) do
          [inner, forward] ->
            ESCH.to_inner_updates(inner, @inner_to_module[inner], forward, value, ctx)
          [field] ->
            to_updates(ctx.assigns.fields, field, value)
        end
      ctx = update(ctx, :fields, &Map.merge(&1, updated_fields))
      broadcast_event(ctx, "update", %{"fields" => updated_fields})
      {:noreply, ctx}
    end

    def to_updates(_fields, name="data_from", value) do
      property = @properties[name]
      fields = %{name => ESCH.to_update(value, property[:type], Access.get(property, :opts))}

      key = "traindata"
      ESCH.update_key_with_module(fields, key, @inner_to_module[key], fn fields, key ->
        fields["data_from"] == key
      end)
    end

    def to_updates(_fields, name, value) do
      property = @properties[name]
      %{name => ESCH.to_update(value, property[:type], Access.get(property, :opts))}
    end

    @impl true
    def to_attrs(%{assigns: %{fields: fields}}) do
      fields
    end

    @impl true
    def to_source(attrs) do
      get_quoted_code(attrs)
      |> Kino.SmartCell.quoted_to_string()
    end

    def get_quoted_code(attrs) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) =
          Evision.ML.SVM.create()
          |> Evision.ML.SVM.setType(unquote(ESCH.quoted_var("Evision.cv_#{attrs["type"]}()")))
          |> Evision.ML.SVM.setKernel(unquote(ESCH.quoted_var("Evision.cv_#{attrs["kernel_type"]}()")))

        unquote(set_svm_param(attrs))
        unquote(set_kernel_param(attrs))
        unquote(set_term_criteria(attrs))
        unquote(train_on_dataset(attrs))
      end
    end

    defp set_svm_param(attrs=%{"type" => "C_SVC"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) = Evision.ML.SVM.setC(unquote(ESCH.quoted_var(attrs["to_variable"])), unquote(attrs["c"]))
      end
    end

    defp set_svm_param(attrs=%{"type" => "NU_SVC"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) = Evision.ML.SVM.setNu(unquote(ESCH.quoted_var(attrs["to_variable"])), unquote(attrs["nu"]))
      end
    end

    defp set_svm_param(attrs=%{"type" => "ONE_CLASS"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) = Evision.ML.SVM.setNu(unquote(ESCH.quoted_var(attrs["to_variable"])), unquote(attrs["nu"]))
      end
    end

    defp set_svm_param(attrs=%{"type" => "EPS_SVR"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) =
          unquote(ESCH.quoted_var(attrs["to_variable"]))
          |> Evision.ML.SVM.setC(unquote(attrs["c"]))
          |> Evision.ML.SVM.setP(unquote(attrs["p"]))
      end
    end

    defp set_svm_param(attrs=%{"type" => "NU_SVR"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) =
          unquote(ESCH.quoted_var(attrs["to_variable"]))
          |> Evision.ML.SVM.setC(unquote(attrs["c"]))
          |> Evision.ML.SVM.setNu(unquote(attrs["nu"]))
      end
    end

    defp set_svm_param(_) do
    end

    defp set_kernel_param(attrs=%{"kernel_type" => "POLY"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) =
          unquote(ESCH.quoted_var(attrs["to_variable"]))
          |> Evision.ML.SVM.setGamma(unquote(attrs["gamma"]))
          |> Evision.ML.SVM.setCoef0(unquote(attrs["coef0"]))
          |> Evision.ML.SVM.setDegree(unquote(attrs["degree"]))
      end
    end

    defp set_kernel_param(attrs=%{"kernel_type" => "RBF"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) = Evision.ML.SVM.setGamma(unquote(ESCH.quoted_var(attrs["to_variable"])), unquote(attrs["gamma"]))
      end
    end

    defp set_kernel_param(attrs=%{"kernel_type" => "SIGMOID"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) =
          unquote(ESCH.quoted_var(attrs["to_variable"]))
          |> Evision.ML.SVM.setGamma(unquote(attrs["gamma"]))
          |> Evision.ML.SVM.setCoef0(unquote(attrs["coef0"]))
      end
    end

    defp set_kernel_param(attrs=%{"kernel_type" => "CHI2"}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) = Evision.ML.SVM.setGamma(unquote(ESCH.quoted_var(attrs["to_variable"])), unquote(attrs["gamma"]))
      end
    end

    defp set_kernel_param(_) do
    end

    defp set_term_criteria(attrs=%{"term_criteria_type" => "max_count", "term_criteria_count" => count, "term_criteria_eps" => eps}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) = Evision.ML.SVM.setTermCriteria(unquote(ESCH.quoted_var(attrs["to_variable"])), {Evision.cv_MAX_ITER(), unquote(count), unquote(eps)})
      end
    end

    defp set_term_criteria(attrs=%{"term_criteria_type" => "eps", "term_criteria_count" => count, "term_criteria_eps" => eps}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) = Evision.ML.SVM.setTermCriteria(unquote(ESCH.quoted_var(attrs["to_variable"])), {Evision.cv_EPS(), unquote(count), unquote(eps)})
      end
    end

    defp set_term_criteria(attrs=%{"term_criteria_type" => "max_count+eps", "term_criteria_count" => count, "term_criteria_eps" => eps}) do
      quote do
        unquote(ESCH.quoted_var(attrs["to_variable"])) = Evision.ML.SVM.setTermCriteria(unquote(ESCH.quoted_var(attrs["to_variable"])), {Evision.cv_MAX_ITER() + Evision.cv_EPS(), unquote(count), unquote(eps)})
      end
    end

    defp train_on_dataset(%{"data_from" => "traindata_var", "traindata_var" => traindata_var, "to_variable" => to_variable}) do
      quote do
        Evision.ML.SVM.train(unquote(ESCH.quoted_var(to_variable)), unquote(ESCH.quoted_var(traindata_var)))
        unquote(TrainData.get_calc_error(Evision.ML.SVM, traindata_var, to_variable))
      end
    end

    defp train_on_dataset(%{"data_from" => "traindata", "traindata" => traindata_attrs, "to_variable" => to_variable}) do
      dataset_variable = traindata_attrs["to_variable"]
      quote do
        unquote(TrainData.get_quoted_code(traindata_attrs))
        Evision.ML.SVM.train(unquote(ESCH.quoted_var(to_variable)), unquote(ESCH.quoted_var(dataset_variable)))
        unquote(TrainData.get_calc_error(Evision.ML.SVM, dataset_variable, to_variable))
      end
    end
  end
end