blob: 6eea812325cd340d7f3edc515d672ecdf8aa4a4a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.ltr.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.lucene.search.Explanation;
import org.apache.solr.ltr.TestRerankBase;
import org.apache.solr.ltr.feature.Feature;
import org.apache.solr.ltr.norm.IdentityNormalizer;
import org.apache.solr.ltr.norm.Normalizer;
import org.apache.solr.util.SolrPluginUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class TestNeuralNetworkModel extends TestRerankBase {
public static LTRScoringModel createNeuralNetworkModel(String name, List<Feature> features,
List<Normalizer> norms,
String featureStoreName, List<Feature> allFeatures,
Map<String,Object> params) throws ModelException {
return LTRScoringModel.getInstance(solrResourceLoader,
NeuralNetworkModel.class.getName(),
name,
features, norms, featureStoreName, allFeatures, params);
}
@Before
public void setup() throws Exception {
setuptest(false);
}
@After
public void after() throws Exception {
aftertest();
}
protected static Map<String,Object> createLayerParams(double[][] matrix, double[] bias, String activation) {
final ArrayList<ArrayList<Double>> matrixList = new ArrayList<ArrayList<Double>>();
for (int row = 0; row < matrix.length; row++) {
matrixList.add(new ArrayList<Double>());
for (int col = 0; col < matrix[row].length; col++) {
matrixList.get(row).add(matrix[row][col]);
}
}
final ArrayList<Double> biasList = new ArrayList<Double>();
for (int i = 0; i < bias.length; i++) {
biasList.add(bias[i]);
}
final Map<String,Object> layer = new HashMap<String,Object>();
layer.put("matrix", matrixList);
layer.put("bias", biasList);
layer.put("activation", activation);
return layer;
}
@Test
public void testLinearAlgebra() {
final double layer1Node1Weight1 = 1.0;
final double layer1Node1Weight2 = 2.0;
final double layer1Node1Weight3 = 3.0;
final double layer1Node1Weight4 = 4.0;
final double layer1Node2Weight1 = 5.0;
final double layer1Node2Weight2 = 6.0;
final double layer1Node2Weight3 = 7.0;
final double layer1Node2Weight4 = 8.0;
final double layer1Node3Weight1 = 9.0;
final double layer1Node3Weight2 = 10.0;
final double layer1Node3Weight3 = 11.0;
final double layer1Node3Weight4 = 12.0;
double[][] matrixOne = { { layer1Node1Weight1, layer1Node1Weight2, layer1Node1Weight3, layer1Node1Weight4 },
{ layer1Node2Weight1, layer1Node2Weight2, layer1Node2Weight3, layer1Node2Weight4 },
{ layer1Node3Weight1, layer1Node3Weight2, layer1Node3Weight3, layer1Node3Weight4 } };
final double layer1Node1Bias = 13.0;
final double layer1Node2Bias = 14.0;
final double layer1Node3Bias = 15.0;
double[] biasOne = { layer1Node1Bias, layer1Node2Bias, layer1Node3Bias };
final double outputNodeWeight1 = 16.0;
final double outputNodeWeight2 = 17.0;
final double outputNodeWeight3 = 18.0;
double[][] matrixTwo = { { outputNodeWeight1, outputNodeWeight2, outputNodeWeight3 } };
final double outputNodeBias = 19.0;
double[] biasTwo = { outputNodeBias };
Map<String,Object> params = new HashMap<String,Object>();
ArrayList<Map<String,Object>> layers = new ArrayList<Map<String,Object>>();
layers.add(createLayerParams(matrixOne, biasOne, "relu"));
layers.add(createLayerParams(matrixTwo, biasTwo, "relu"));
params.put("layers", layers);
final List<Feature> allFeaturesInStore
= getFeatures(new String[] {"constantOne", "constantTwo", "constantThree", "constantFour", "constantFive"});
final List<Feature> featuresInModel = new ArrayList<>(allFeaturesInStore);
Collections.shuffle(featuresInModel, random()); // store and model order of features can vary
featuresInModel.remove(0); // models need not use all the store's features
assertEquals(4, featuresInModel.size()); // the test model uses four features
final List<Normalizer> norms =
new ArrayList<Normalizer>(
Collections.nCopies(featuresInModel.size(),IdentityNormalizer.INSTANCE));
final LTRScoringModel ltrScoringModel = createNeuralNetworkModel("test_score",
featuresInModel, norms, "test_score", allFeaturesInStore, params);
{
// pretend all features scored zero
float[] testVec = { 0.0f, 0.0f, 0.0f, 0.0f };
// with all zero inputs the layer1 node outputs are layer1 node biases only
final double layer1Node1Output = layer1Node1Bias;
final double layer1Node2Output = layer1Node2Bias;
final double layer1Node3Output = layer1Node3Bias;
// with just one layer the output node calculation is easy
final double outputNodeOutput =
(layer1Node1Output*outputNodeWeight1) +
(layer1Node2Output*outputNodeWeight2) +
(layer1Node3Output*outputNodeWeight3) +
outputNodeBias;
assertEquals(735.0, outputNodeOutput, 0.001);
// and the expected score is that of the output node
final double expectedScore = outputNodeOutput;
float score = ltrScoringModel.score(testVec);
assertEquals(expectedScore, score, 0.001);
}
{
// pretend all features scored one
float[] testVec = { 1.0f, 1.0f, 1.0f, 1.0f };
// with all one inputs the layer1 node outputs are simply sum of weights and biases
final double layer1Node1Output = layer1Node1Weight1 + layer1Node1Weight2 + layer1Node1Weight3 + layer1Node1Weight4 + layer1Node1Bias;
final double layer1Node2Output = layer1Node2Weight1 + layer1Node2Weight2 + layer1Node2Weight3 + layer1Node2Weight4 + layer1Node2Bias;
final double layer1Node3Output = layer1Node3Weight1 + layer1Node3Weight2 + layer1Node3Weight3 + layer1Node3Weight4 + layer1Node3Bias;
// with just one layer the output node calculation is easy
final double outputNodeOutput =
(layer1Node1Output*outputNodeWeight1) +
(layer1Node2Output*outputNodeWeight2) +
(layer1Node3Output*outputNodeWeight3) +
outputNodeBias;
assertEquals(2093.0, outputNodeOutput, 0.001);
// and the expected score is that of the output node
final double expectedScore = outputNodeOutput;
float score = ltrScoringModel.score(testVec);
assertEquals(expectedScore, score, 0.001);
}
{
// pretend all features scored random numbers in 0.0 to 1.0 range
final float input1 = random().nextFloat();
final float input2 = random().nextFloat();
final float input3 = random().nextFloat();
final float input4 = random().nextFloat();
float[] testVec = {input1, input2, input3, input4};
// the layer1 node outputs are sum of input-times-weight plus bias
final double layer1Node1Output = input1*layer1Node1Weight1 + input2*layer1Node1Weight2 + input3*layer1Node1Weight3 + input4*layer1Node1Weight4 + layer1Node1Bias;
final double layer1Node2Output = input1*layer1Node2Weight1 + input2*layer1Node2Weight2 + input3*layer1Node2Weight3 + input4*layer1Node2Weight4 + layer1Node2Bias;
final double layer1Node3Output = input1*layer1Node3Weight1 + input2*layer1Node3Weight2 + input3*layer1Node3Weight3 + input4*layer1Node3Weight4 + layer1Node3Bias;
// with just one layer the output node calculation is easy
final double outputNodeOutput =
(layer1Node1Output*outputNodeWeight1) +
(layer1Node2Output*outputNodeWeight2) +
(layer1Node3Output*outputNodeWeight3) +
outputNodeBias;
assertTrue("outputNodeOutput="+outputNodeOutput, 735.0 <= outputNodeOutput); // inputs between zero and one produced output greater than 74
assertTrue("outputNodeOutput="+outputNodeOutput, outputNodeOutput <= 2093.0); // inputs between zero and one produced output less than 294
// and the expected score is that of the output node
final double expectedScore = outputNodeOutput;
float score = ltrScoringModel.score(testVec);
assertEquals(expectedScore, score, 0.001);
}
}
@Test
public void badActivationTest() throws Exception {
final ModelException expectedException =
new ModelException("Invalid activation function (\"sig\") in layer 0 of model \"neuralnetworkmodel_bad_activation\".");
Exception ex = expectThrows(Exception.class, () -> {
createModelFromFiles("neuralnetworkmodel_bad_activation.json",
"neuralnetworkmodel_features.json");
});
Throwable rootError = getRootCause(ex);
assertEquals(expectedException.toString(), rootError.toString());
}
@Test
public void biasDimensionMismatchTest() throws Exception {
final ModelException expectedException =
new ModelException("Dimension mismatch in model \"neuralnetworkmodel_mismatch_bias\". " +
"Layer 0 has 2 bias weights but 3 weight matrix rows.");
Exception ex = expectThrows(Exception.class, () -> {
createModelFromFiles("neuralnetworkmodel_mismatch_bias.json",
"neuralnetworkmodel_features.json");
});
Throwable rootError = getRootCause(ex);
assertEquals(expectedException.toString(), rootError.toString());
}
@Test
public void inputDimensionMismatchTest() throws Exception {
final ModelException expectedException =
new ModelException("Dimension mismatch in model \"neuralnetworkmodel_mismatch_input\". The input has " +
"4 features, but the weight matrix for layer 0 has 3 columns.");
Exception ex = expectThrows(Exception.class, () -> {
createModelFromFiles("neuralnetworkmodel_mismatch_input.json",
"neuralnetworkmodel_features.json");
});
Throwable rootError = getRootCause(ex);
assertEquals(expectedException.toString(), rootError.toString());
}
@Test
public void layerDimensionMismatchTest() throws Exception {
final ModelException expectedException =
new ModelException("Dimension mismatch in model \"neuralnetworkmodel_mismatch_layers\". The weight matrix " +
"for layer 0 has 2 rows, but the weight matrix for layer 1 has 3 columns.");
Exception ex = expectThrows(Exception.class, () -> {
createModelFromFiles("neuralnetworkmodel_mismatch_layers.json",
"neuralnetworkmodel_features.json");
});
Throwable rootError = getRootCause(ex);
assertEquals(expectedException.toString(), rootError.toString());
}
@Test
public void tooManyRowsTest() throws Exception {
final ModelException expectedException =
new ModelException("Dimension mismatch in model \"neuralnetworkmodel_too_many_rows\". " +
"Layer 1 has 1 bias weights but 2 weight matrix rows.");
Exception ex = expectThrows(Exception.class, () -> {
createModelFromFiles("neuralnetworkmodel_too_many_rows.json",
"neuralnetworkmodel_features.json");
});
Throwable rootError = getRootCause(ex);
assertEquals(expectedException.toString(), rootError.toString());
}
@Test
public void testExplain() throws Exception {
final LTRScoringModel model = createModelFromFiles("neuralnetworkmodel_explainable.json",
"neuralnetworkmodel_features.json");
final float[] featureValues = { 1.2f, 3.4f, 5.6f, 7.8f };
final List<Explanation> explanations = new ArrayList<Explanation>();
for (int ii=0; ii<featureValues.length; ++ii)
{
explanations.add(Explanation.match(featureValues[ii], ""));
}
final float finalScore = model.score(featureValues);
final Explanation explanation = model.explain(null, 0, finalScore, explanations);
assertEquals(finalScore+" = (name=neuralnetworkmodel_explainable"+
",featureValues=[constantOne=1.2,constantTwo=3.4,constantThree=5.6,constantFour=7.8]"+
",layers=[(matrix=2x4,activation=relu),(matrix=1x2,activation=identity)]"+
")\n",
explanation.toString());
}
public static class CustomNeuralNetworkModel extends NeuralNetworkModel {
public CustomNeuralNetworkModel(String name, List<Feature> features, List<Normalizer> norms,
String featureStoreName, List<Feature> allFeatures, Map<String,Object> params) {
super(name, features, norms, featureStoreName, allFeatures, params);
}
public class DefaultLayer extends org.apache.solr.ltr.model.NeuralNetworkModel.DefaultLayer {
@Override
public void setActivation(Object o) {
super.setActivation(o);
switch (this.activationStr) {
case "answer":
this.activation = new Activation() {
@Override
public float apply(float in) {
return in * 42f;
}
};
break;
default:
break;
}
}
}
@Override
@SuppressWarnings({"unchecked"})
protected Layer createLayer(Object o) {
final DefaultLayer layer = new DefaultLayer();
if (o != null) {
SolrPluginUtils.invokeSetters(layer, ((Map<String,Object>) o).entrySet());
}
return layer;
}
}
@Test
public void testCustom() throws Exception {
final LTRScoringModel model = createModelFromFiles("neuralnetworkmodel_custom.json",
"neuralnetworkmodel_features.json");
final float featureValue1 = 4f;
final float featureValue2 = 2f;
final float[] featureValues = { featureValue1, featureValue2 };
final double expectedScore = (featureValue1+featureValue2) * 42f;
float actualScore = model.score(featureValues);
assertEquals(expectedScore, actualScore, 0.001);
final List<Explanation> explanations = new ArrayList<Explanation>();
for (int ii=0; ii<featureValues.length; ++ii)
{
explanations.add(Explanation.match(featureValues[ii], ""));
}
final Explanation explanation = model.explain(null, 0, actualScore, explanations);
assertEquals(actualScore+" = (name=neuralnetworkmodel_custom"+
",featureValues=[constantFour=4.0,constantTwo=2.0]"+
",layers=[(matrix=1x2,activation=answer)]"+
")\n",
explanation.toString());
}
}