Save And Load an External Model

We would like to assume you would like to implement a learner that learns a certain model from the data and that you would subsequently like to have a predictor node that uses the learned model in order to classify new data. To accomplish this, you need a facility to pass the learned model from the learner to the predictor. The KNIME framework provides this functionality using the ModelPort concept. To roughly explain this concept we will now create the learned model for our numeric binner and write the interval bounds for every bin into a model. For this purpose it is necessary to overwrite the saveModelContent method of the NodeModel. If we had a node with a ModelInport we would have to overwrite the loadModelContent method in order to be able to load the model when it is connected to the inport. Although it is possible to write your model directly to the ModelContent object it is good practice and highly recommended to have an object that represents the external model of your node and which is solely responsible for the loading from and saving to the ModelContent. In our case we create a class NumericBinModel. It is then very easy to share your model with other nodes. To increase the usability of the model we store the lower and upper bound as an interval for every bin. Since we only have a ModelOutport we only have to implement the saveTo method, as can be seen below:

	/**
     * Saves this model to the model content
     * @param modelContent the model content to save to.
     */
    public void saveTo(final ModelContentWO modelContent) {
        modelContent.addInt(NUMBER_OF_BINS, m_intervals.size());
        int intervalNr = 0;
        for (Interval interval : m_intervals) {
            ModelContentWO intervalModel = modelContent.addModelContent(
                    INTERVAL + intervalNr++);
            intervalModel.addDouble(LOWER_BOUND, interval.getLowerBound());
            intervalModel.addDouble(UPPER_BOUND, interval.getUpperBound());
        }
    }

The interval simply stores the lower bound upper bound pair. In addition the model provides some methods to add intervals and retrieve the intervals:

	/**
     * 
     * @param binNumber the number of the bin for which the lower bound 
     * should be returned
     * @return the lower bound of the specified interval.
     */
    public double getLowerBoundForInterval(final int binNumber) {
        return m_intervals.get(binNumber).getLowerBound();
    }

    /**
     * 
     * @param binNumber the number of the bin for which the upper bound 
     * should be returned
     * @return the upper bound of the specified interval.
     */
    public double getUpperBoundForInterval(final int binNumber) {
        return m_intervals.get(binNumber).getUpperBound();
    }
    
    /**
     * 
     * @return the number of bins, i.e. intervals.
     */
    public int getNumberOfBins() {
        return m_intervals.size();
    }
    

    /**
     * Adds an interval to this model.
     * @param lowerBound the lower bound of the interval
     * @param upperBound the upper bound of the interval
     */
    public void addInterval(final double lowerBound, final double upperBound) {
        Interval interval = new Interval(lowerBound, upperBound);
        m_intervals.add(interval);
    }

Now, we have to fill the model in the for-loop of the NodeModel's execute method:

	...
        double intervalUpperBound = lowerBound;
        // create the external model
        m_model = new NumericBinModel();
        double intervalLowerBound = lowerBound;
        for (int i = 0; i < m_numberOfBins.getIntValue(); i++) {
            intervalLowerBound = intervalUpperBound;
            intervalUpperBound += interval;
            // fill the external model
            m_model.addInterval(intervalLowerBound, intervalUpperBound);
            splitPoints.add(intervalUpperBound);
            // fill the bins with empty representations
            m_bins[i] = new NumericBin();
        }
        ...

Once the model has been filled with the information about the intervals it is possible to save it in the saveModelContent method of the NodeModel:

	/**
     * Only the saveModelContent method has to be overwritten, since there is 
     * only a ModelOutport.
     * 
     * @see de.unikn.knime.core.node.NodeModel#saveModelContent(int, 
     * de.unikn.knime.core.node.ModelContentWO)
     */
    @Override
    protected void saveModelContent(final int index, 
            final ModelContentWO modelContent) throws InvalidSettingsException {
        m_model.saveTo(modelContent);
    }

Since we now provide an external model of our node we have to add a ModelOutport. This is completed in the constructor of the NodeModel, where the number of DataIn- and Outports is defined with the first two parameters and the number of ModelIn- and Outports with the last two parameters. Since we have no ModelInport and one ModelOutport the resulting new constructor looks like this:

	/**
     * Constructor for the node model.
     */
    protected NumericBinnerNodeModel() {
        // we have one inport for the numeric data to bin
        // and two outports:
        // one for the original data with the binning information appended
        // and one for the bins and their used interval bounds.
        super(1, 1, 0, 1);
    }

Having adapted your constructor in this way you will notice the following error the next time you start your workbench:

ERROR   NumericBinnerNodeFactory   CODING PROBLEM   Missing or surplus predictor output port name

To avoid this you have to adapt your node description, which is explained in the next section.