mirror of https://github.com/apache/lucene.git
SOLR-11597: Add contrib/ltr NeuralNetworkModel class.
(Michael A. Alcorn, Yuki Yano, Christine Poerschke) Closes #270
This commit is contained in:
parent
c59e2e98d2
commit
c5938f79e5
|
@ -170,6 +170,8 @@ New Features
|
|||
|
||||
* SOLR-12006: Add a '*_t' and '*_t_sort' dynamic field for single valued text fields (Varun Thacker)
|
||||
|
||||
* SOLR-11597: Add contrib/ltr NeuralNetworkModel class. (Michael A. Alcorn, Yuki Yano, Christine Poerschke)
|
||||
|
||||
Bug Fixes
|
||||
----------------------
|
||||
|
||||
|
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* 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.lang.Math;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.search.Explanation;
|
||||
import org.apache.solr.ltr.feature.Feature;
|
||||
import org.apache.solr.ltr.norm.Normalizer;
|
||||
import org.apache.solr.util.SolrPluginUtils;
|
||||
|
||||
/**
|
||||
* A scoring model that computes document scores using a neural network.
|
||||
* <p>
|
||||
* Supported <a href="https://en.wikipedia.org/wiki/Activation_function">activation functions</a> are:
|
||||
* <code>identity</code>, <code>relu</code>, <code>sigmoid</code> and
|
||||
* contributions to support additional activation functions are welcome.
|
||||
* <p>
|
||||
* Example configuration:
|
||||
<pre>{
|
||||
"class" : "org.apache.solr.ltr.model.NeuralNetworkModel",
|
||||
"name" : "rankNetModel",
|
||||
"features" : [
|
||||
{ "name" : "documentRecency" },
|
||||
{ "name" : "isBook" },
|
||||
{ "name" : "originalScore" }
|
||||
],
|
||||
"params" : {
|
||||
"layers" : [
|
||||
{
|
||||
"matrix" : [ [ 1.0, 2.0, 3.0 ],
|
||||
[ 4.0, 5.0, 6.0 ],
|
||||
[ 7.0, 8.0, 9.0 ],
|
||||
[ 10.0, 11.0, 12.0 ] ],
|
||||
"bias" : [ 13.0, 14.0, 15.0, 16.0 ],
|
||||
"activation" : "sigmoid"
|
||||
},
|
||||
{
|
||||
"matrix" : [ [ 17.0, 18.0, 19.0, 20.0 ],
|
||||
[ 21.0, 22.0, 23.0, 24.0 ] ],
|
||||
"bias" : [ 25.0, 26.0 ],
|
||||
"activation" : "relu"
|
||||
},
|
||||
{
|
||||
"matrix" : [ [ 27.0, 28.0 ] ],
|
||||
"bias" : [ 29.0 ],
|
||||
"activation" : "identity"
|
||||
}
|
||||
]
|
||||
}
|
||||
}</pre>
|
||||
* <p>
|
||||
* Training libraries:
|
||||
* <ul>
|
||||
* <li> <a href="https://keras.io">Keras</a> is a high-level neural networks API, written in Python.
|
||||
* A Keras and Solr implementation of RankNet can be found at <a href="https://github.com/airalcorn2/RankNet">https://github.com/airalcorn2/RankNet</a>.
|
||||
* </ul>
|
||||
* <p>
|
||||
* Background reading:
|
||||
* <ul>
|
||||
* <li> <a href="http://icml.cc/2015/wp-content/uploads/2015/06/icml_ranking.pdf">
|
||||
* C. Burges, T. Shaked, E. Renshaw, A. Lazier, M. Deeds, N. Hamilton, and G. Hullender. Learning to Rank Using Gradient Descent.
|
||||
* Proceedings of the 22nd International Conference on Machine Learning (ICML), ACM, 2005.</a>
|
||||
* </ul>
|
||||
*/
|
||||
public class NeuralNetworkModel extends LTRScoringModel {
|
||||
|
||||
private List<Layer> layers;
|
||||
|
||||
protected interface Activation {
|
||||
// similar to UnaryOperator<Float>
|
||||
float apply(float in);
|
||||
}
|
||||
|
||||
public interface Layer {
|
||||
public float[] calculateOutput(float[] inputVec);
|
||||
public int validate(int inputDim) throws ModelException;
|
||||
public String describe();
|
||||
}
|
||||
|
||||
public class DefaultLayer implements Layer {
|
||||
private int layerID;
|
||||
private float[][] weightMatrix;
|
||||
private int matrixRows;
|
||||
private int matrixCols;
|
||||
private float[] biasVector;
|
||||
private int numUnits;
|
||||
protected String activationStr;
|
||||
protected Activation activation;
|
||||
|
||||
public DefaultLayer() {
|
||||
layerID = layers.size();
|
||||
}
|
||||
|
||||
public void setMatrix(Object matrixObj) {
|
||||
final List<List<Double>> matrix = (List<List<Double>>) matrixObj;
|
||||
this.matrixRows = matrix.size();
|
||||
this.matrixCols = matrix.get(0).size();
|
||||
this.weightMatrix = new float[this.matrixRows][this.matrixCols];
|
||||
|
||||
for (int i = 0; i < this.matrixRows; i++) {
|
||||
for (int j = 0; j < this.matrixCols; j++) {
|
||||
this.weightMatrix[i][j] = matrix.get(i).get(j).floatValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setBias(Object biasObj) {
|
||||
final List<Double> vector = (List<Double>) biasObj;
|
||||
this.numUnits = vector.size();
|
||||
this.biasVector = new float[numUnits];
|
||||
|
||||
for (int i = 0; i < this.numUnits; i++) {
|
||||
this.biasVector[i] = vector.get(i).floatValue();
|
||||
}
|
||||
}
|
||||
|
||||
public void setActivation(Object activationStr) {
|
||||
this.activationStr = (String) activationStr;
|
||||
switch (this.activationStr) {
|
||||
case "relu":
|
||||
this.activation = new Activation() {
|
||||
@Override
|
||||
public float apply(float in) {
|
||||
return in < 0 ? 0 : in;
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "sigmoid":
|
||||
this.activation = new Activation() {
|
||||
@Override
|
||||
public float apply(float in) {
|
||||
return (float) (1 / (1 + Math.exp(-in)));
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "identity":
|
||||
this.activation = new Activation() {
|
||||
@Override
|
||||
public float apply(float in) {
|
||||
return in;
|
||||
}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
this.activation = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public float[] calculateOutput(float[] inputVec) {
|
||||
|
||||
float[] outputVec = new float[this.matrixRows];
|
||||
|
||||
for (int i = 0; i < this.matrixRows; i++) {
|
||||
float outputVal = this.biasVector[i];
|
||||
for (int j = 0; j < this.matrixCols; j++) {
|
||||
outputVal += this.weightMatrix[i][j] * inputVec[j];
|
||||
}
|
||||
outputVec[i] = this.activation.apply(outputVal);
|
||||
}
|
||||
|
||||
return outputVec;
|
||||
}
|
||||
|
||||
public int validate(int inputDim) throws ModelException {
|
||||
if (this.numUnits != this.matrixRows) {
|
||||
throw new ModelException("Dimension mismatch in model \"" + name + "\". Layer " +
|
||||
Integer.toString(this.layerID) + " has " + Integer.toString(this.numUnits) +
|
||||
" bias weights but " + Integer.toString(this.matrixRows) + " weight matrix rows.");
|
||||
}
|
||||
if (this.activation == null) {
|
||||
throw new ModelException("Invalid activation function (\""+this.activationStr+"\") in layer "+Integer.toString(this.layerID)+" of model \"" + name + "\".");
|
||||
}
|
||||
if (inputDim != this.matrixCols) {
|
||||
if (this.layerID == 0) {
|
||||
throw new ModelException("Dimension mismatch in model \"" + name + "\". The input has " +
|
||||
Integer.toString(inputDim) + " features, but the weight matrix for layer 0 has " +
|
||||
Integer.toString(this.matrixCols) + " columns.");
|
||||
} else {
|
||||
throw new ModelException("Dimension mismatch in model \"" + name + "\". The weight matrix for layer " +
|
||||
Integer.toString(this.layerID - 1) + " has " + Integer.toString(inputDim) + " rows, but the " +
|
||||
"weight matrix for layer " + Integer.toString(this.layerID) + " has " +
|
||||
Integer.toString(this.matrixCols) + " columns.");
|
||||
}
|
||||
}
|
||||
return this.matrixRows;
|
||||
}
|
||||
|
||||
public String describe() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb
|
||||
.append("(matrix=").append(Integer.toString(this.matrixRows)).append('x').append(Integer.toString(this.matrixCols))
|
||||
.append(",activation=").append(this.activationStr).append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
protected Layer createLayer(Object o) {
|
||||
final DefaultLayer layer = new DefaultLayer();
|
||||
if (o != null) {
|
||||
SolrPluginUtils.invokeSetters(layer, ((Map<String,Object>) o).entrySet());
|
||||
}
|
||||
return layer;
|
||||
}
|
||||
|
||||
public void setLayers(Object layers) {
|
||||
this.layers = new ArrayList<Layer>();
|
||||
for (final Object o : (List<Object>) layers) {
|
||||
final Layer layer = createLayer(o);
|
||||
this.layers.add(layer);
|
||||
}
|
||||
}
|
||||
|
||||
public NeuralNetworkModel(String name, List<Feature> features,
|
||||
List<Normalizer> norms,
|
||||
String featureStoreName, List<Feature> allFeatures,
|
||||
Map<String,Object> params) {
|
||||
super(name, features, norms, featureStoreName, allFeatures, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void validate() throws ModelException {
|
||||
super.validate();
|
||||
|
||||
int inputDim = features.size();
|
||||
|
||||
for (Layer layer : layers) {
|
||||
inputDim = layer.validate(inputDim);
|
||||
}
|
||||
|
||||
if (inputDim != 1) {
|
||||
throw new ModelException("The output matrix for model \"" + name + "\" has " + Integer.toString(inputDim) +
|
||||
" rows, but should only have one.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float score(float[] inputFeatures) {
|
||||
|
||||
float[] outputVec = inputFeatures;
|
||||
|
||||
for (Layer layer : layers) {
|
||||
outputVec = layer.calculateOutput(outputVec);
|
||||
}
|
||||
|
||||
return outputVec[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Explanation explain(LeafReaderContext context, int doc,
|
||||
float finalScore, List<Explanation> featureExplanations) {
|
||||
|
||||
final StringBuilder modelDescription = new StringBuilder();
|
||||
|
||||
modelDescription.append("(name=").append(getName());
|
||||
modelDescription.append(",featureValues=[");
|
||||
|
||||
for (int i = 0; i < featureExplanations.size(); i++) {
|
||||
Explanation featureExplain = featureExplanations.get(i);
|
||||
if (i > 0) {
|
||||
modelDescription.append(',');
|
||||
}
|
||||
final String key = features.get(i).getName();
|
||||
modelDescription.append(key).append('=').append(featureExplain.getValue());
|
||||
}
|
||||
|
||||
modelDescription.append("],layers=[");
|
||||
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
if (i > 0) modelDescription.append(',');
|
||||
modelDescription.append(layers.get(i).describe());
|
||||
}
|
||||
modelDescription.append("])");
|
||||
|
||||
return Explanation.match(finalScore, modelDescription.toString());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"name": "constantOne",
|
||||
"class": "org.apache.solr.ltr.feature.ValueFeature",
|
||||
"params": {
|
||||
"value": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "constantTwo",
|
||||
"class": "org.apache.solr.ltr.feature.ValueFeature",
|
||||
"params": {
|
||||
"value": 2.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "constantThree",
|
||||
"class": "org.apache.solr.ltr.feature.ValueFeature",
|
||||
"params": {
|
||||
"value": 3.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "constantFour",
|
||||
"class": "org.apache.solr.ltr.feature.ValueFeature",
|
||||
"params": {
|
||||
"value": 4.0
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"class":"org.apache.solr.ltr.model.NeuralNetworkModel",
|
||||
"name":"neuralnetworkmodel_bad_activation",
|
||||
"features":[
|
||||
{ "name": "constantOne"},
|
||||
{ "name": "constantTwo"},
|
||||
{ "name": "constantThree"},
|
||||
{ "name": "constantFour"}
|
||||
],
|
||||
"params":{
|
||||
"layers": [
|
||||
{
|
||||
"matrix": [ [ 1.0, 2.0, 3.0, 4.0 ],
|
||||
[ 5.0, 6.0, 7.0, 8.0 ],
|
||||
[ 9.0, 10.0, 11.0, 12.0 ] ],
|
||||
"bias" : [ 13.0, 14.0, 15.0 ],
|
||||
"activation": "sig"
|
||||
},
|
||||
{
|
||||
"matrix": [ [ 16.0, 17.0, 18.0 ] ],
|
||||
"bias" : [ 19.0 ],
|
||||
"activation": "identity"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"class":"org.apache.solr.ltr.model.TestNeuralNetworkModel$CustomNeuralNetworkModel",
|
||||
"name":"neuralnetworkmodel_custom",
|
||||
"features":[
|
||||
{ "name": "constantFour"},
|
||||
{ "name": "constantTwo"}
|
||||
],
|
||||
"params":{
|
||||
"layers": [
|
||||
{
|
||||
"matrix": [ [ 1.0, 1.0 ] ],
|
||||
"bias" : [ 0.0 ],
|
||||
"activation": "answer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"class":"org.apache.solr.ltr.model.NeuralNetworkModel",
|
||||
"name":"neuralnetworkmodel_explainable",
|
||||
"features":[
|
||||
{ "name": "constantOne"},
|
||||
{ "name": "constantTwo"},
|
||||
{ "name": "constantThree"},
|
||||
{ "name": "constantFour"}
|
||||
],
|
||||
"params":{
|
||||
"layers": [
|
||||
{
|
||||
"matrix": [
|
||||
[ 11.0, 2.0, 33.0, 4.0 ],
|
||||
[ 1.0, 22.0, 3.0, 44.0 ] ],
|
||||
"bias" : [ 55.0, 66.0 ],
|
||||
"activation": "relu"
|
||||
},
|
||||
{
|
||||
"matrix": [ [ 11.0, 22.0 ] ],
|
||||
"bias" : [ 77.0 ],
|
||||
"activation": "identity"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"class":"org.apache.solr.ltr.model.NeuralNetworkModel",
|
||||
"name":"neuralnetworkmodel_mismatch_bias",
|
||||
"features":[
|
||||
{ "name": "constantOne"},
|
||||
{ "name": "constantTwo"},
|
||||
{ "name": "constantThree"},
|
||||
{ "name": "constantFour"}
|
||||
],
|
||||
"params":{
|
||||
"layers": [
|
||||
{
|
||||
"matrix": [ [ 1.0, 2.0, 3.0, 4.0 ],
|
||||
[ 5.0, 6.0, 7.0, 8.0 ],
|
||||
[ 9.0, 10.0, 11.0, 12.0 ] ],
|
||||
"bias" : [ 13.0, 14.0 ],
|
||||
"activation": "relu"
|
||||
},
|
||||
{
|
||||
"matrix": [ [ 16.0, 17.0, 18.0 ] ],
|
||||
"bias" : [ 19.0 ],
|
||||
"activation": "identity"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"class":"org.apache.solr.ltr.model.NeuralNetworkModel",
|
||||
"name":"neuralnetworkmodel_mismatch_input",
|
||||
"features":[
|
||||
{ "name": "constantOne"},
|
||||
{ "name": "constantTwo"},
|
||||
{ "name": "constantThree"},
|
||||
{ "name": "constantFour"}
|
||||
],
|
||||
"params":{
|
||||
"layers": [
|
||||
{
|
||||
"matrix": [ [ 1.0, 2.0, 3.0 ],
|
||||
[ 5.0, 6.0, 7.0 ],
|
||||
[ 9.0, 10.0, 11.0 ] ],
|
||||
"bias" : [ 13.0, 14.0, 15.0 ],
|
||||
"activation": "relu"
|
||||
},
|
||||
{
|
||||
"matrix": [ [ 16.0, 17.0, 18.0 ] ],
|
||||
"bias" : [ 19.0 ],
|
||||
"activation": "identity"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"class":"org.apache.solr.ltr.model.NeuralNetworkModel",
|
||||
"name":"neuralnetworkmodel_mismatch_layers",
|
||||
"features":[
|
||||
{ "name": "constantOne"},
|
||||
{ "name": "constantTwo"},
|
||||
{ "name": "constantThree"},
|
||||
{ "name": "constantFour"}
|
||||
],
|
||||
"params":{
|
||||
"layers": [
|
||||
{
|
||||
"matrix": [ [ 1.0, 2.0, 3.0, 4.0 ],
|
||||
[ 5.0, 6.0, 7.0, 8.0 ] ],
|
||||
"bias" : [ 13.0, 14.0 ],
|
||||
"activation": "relu"
|
||||
},
|
||||
{
|
||||
"matrix": [ [ 16.0, 17.0, 18.0 ] ],
|
||||
"bias" : [ 19.0 ],
|
||||
"activation": "identity"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"class":"org.apache.solr.ltr.model.NeuralNetworkModel",
|
||||
"name":"neuralnetworkmodel_too_many_rows",
|
||||
"features":[
|
||||
{ "name": "constantOne"},
|
||||
{ "name": "constantTwo"},
|
||||
{ "name": "constantThree"},
|
||||
{ "name": "constantFour"}
|
||||
],
|
||||
"params":{
|
||||
"layers": [
|
||||
{
|
||||
"matrix": [ [ 1.0, 2.0, 3.0, 4.0 ],
|
||||
[ 5.0, 6.0, 7.0, 8.0 ],
|
||||
[ 9.0, 10.0, 11.0, 12.0 ] ],
|
||||
"bias" : [ 13.0, 14.0, 15.0 ],
|
||||
"activation": "relu"
|
||||
},
|
||||
{
|
||||
"matrix": [ [ 16.0, 17.0, 18.0 ],
|
||||
[ 19.0, 20.0, 21.0 ] ],
|
||||
"bias" : [ 19.0 ],
|
||||
"activation": "identity"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,365 @@
|
|||
/*
|
||||
* 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.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
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);
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() throws Exception {
|
||||
setuptest(false);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static 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\".");
|
||||
try {
|
||||
createModelFromFiles("neuralnetworkmodel_bad_activation.json",
|
||||
"neuralnetworkmodel_features.json");
|
||||
fail("badActivationTest failed to throw exception: "+expectedException);
|
||||
} catch (Exception actualException) {
|
||||
Throwable rootError = getRootCause(actualException);
|
||||
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.");
|
||||
try {
|
||||
createModelFromFiles("neuralnetworkmodel_mismatch_bias.json",
|
||||
"neuralnetworkmodel_features.json");
|
||||
fail("biasDimensionMismatchTest failed to throw exception: "+expectedException);
|
||||
} catch (Exception actualException) {
|
||||
Throwable rootError = getRootCause(actualException);
|
||||
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.");
|
||||
try {
|
||||
createModelFromFiles("neuralnetworkmodel_mismatch_input.json",
|
||||
"neuralnetworkmodel_features.json");
|
||||
fail("inputDimensionMismatchTest failed to throw exception: "+expectedException);
|
||||
} catch (Exception actualException) {
|
||||
Throwable rootError = getRootCause(actualException);
|
||||
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.");
|
||||
try {
|
||||
createModelFromFiles("neuralnetworkmodel_mismatch_layers.json",
|
||||
"neuralnetworkmodel_features.json");
|
||||
fail("layerDimensionMismatchTest failed to throw exception: "+expectedException);
|
||||
} catch (Exception actualException) {
|
||||
Throwable rootError = getRootCause(actualException);
|
||||
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.");
|
||||
try {
|
||||
createModelFromFiles("neuralnetworkmodel_too_many_rows.json",
|
||||
"neuralnetworkmodel_features.json");
|
||||
fail("layerDimensionMismatchTest failed to throw exception: "+expectedException);
|
||||
} catch (Exception actualException) {
|
||||
Throwable rootError = getRootCause(actualException);
|
||||
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
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
|
@ -87,6 +87,7 @@ Feature selection and model training take place offline and outside Solr. The lt
|
|||
|General form |Class |Specific examples
|
||||
|Linear |{solr-javadocs}/solr-ltr/org/apache/solr/ltr/model/LinearModel.html[LinearModel] |RankSVM, Pranking
|
||||
|Multiple Additive Trees |{solr-javadocs}/solr-ltr/org/apache/solr/ltr/model/MultipleAdditiveTreesModel.html[MultipleAdditiveTreesModel] |LambdaMART, Gradient Boosted Regression Trees (GBRT)
|
||||
|Neural Network |{solr-javadocs}/solr-ltr/org/apache/solr/ltr/model/NeuralNetworkModel.html[NeuralNetworkModel] |RankNet
|
||||
|(wrapper) |{solr-javadocs}/solr-ltr/org/apache/solr/ltr/model/DefaultWrapperModel.html[DefaultWrapperModel] |(not applicable)
|
||||
|(custom) |(custom class extending {solr-javadocs}/solr-ltr/org/apache/solr/ltr/model/AdapterModel.html[AdapterModel]) |(not applicable)
|
||||
|(custom) |(custom class extending {solr-javadocs}/solr-ltr/org/apache/solr/ltr/model/LTRScoringModel.html[LTRScoringModel]) |(not applicable)
|
||||
|
|
Loading…
Reference in New Issue