Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8d07401
Attempt on Issue 4169
mstfbl Sep 18, 2019
d80e23d
Further work on Issue_4169
mstfbl Sep 20, 2019
7b239b0
Temporary change for inquiry
mstfbl Sep 24, 2019
33e71ae
Pushing changes for inquiry
mstfbl Sep 26, 2019
2b1ab69
Implemented PredictedLabel as Categorical value (String/int). Now wor…
mstfbl Sep 26, 2019
7254a7e
Merge branch 'Issue_4169' of https://github.com/mstfbl/machinelearnin…
mstfbl Sep 26, 2019
1a43cc7
Added tests for Prediction Engine
mstfbl Sep 26, 2019
d39f98b
Removed the forwarding of DataViewSchema to the TrySinglePrediction f…
mstfbl Sep 26, 2019
8cf2b7c
Minor performance upgrade to avoid the array bounds checkl
mstfbl Sep 27, 2019
c4182fb
Update ImageClassificationTransform.cs
mstfbl Sep 30, 2019
d490383
Updated tests
mstfbl Oct 2, 2019
01c77c9
Merge branch 'Issue_4169' of https://github.com/mstfbl/machinelearnin…
mstfbl Oct 2, 2019
380d8bf
Revert "Merge branch 'Issue_4169' of https://github.com/mstfbl/machin…
mstfbl Oct 2, 2019
2af03ca
Revert "Updated tests"
mstfbl Oct 2, 2019
2c05ba8
Updated test files and corrected variable spellings
mstfbl Oct 2, 2019
6356665
Merge branch 'master' of https://github.com/dotnet/machinelearning in…
mstfbl Oct 2, 2019
ed928c6
Update ImageClassificationTransform.cs
mstfbl Oct 2, 2019
f7f8253
Merge branch 'master' of https://github.com/dotnet/machinelearning in…
mstfbl Oct 2, 2019
bd42f0a
Revert "Merge branch 'master' of https://github.com/dotnet/machinelea…
mstfbl Oct 2, 2019
f851791
Merge branch 'master' into Issue_4169
mstfbl Oct 2, 2019
643fb58
Deleted unused _outputTypes
mstfbl Oct 2, 2019
b6cfeda
Update ImageClassificationTransform.cs
mstfbl Oct 2, 2019
cca4fb8
Added test case to check the matching of predicted labels
mstfbl Oct 2, 2019
1c4c5dc
Update ImageClassificationTransform.cs
mstfbl Oct 2, 2019
2301a96
Update TensorflowTests.cs
mstfbl Oct 2, 2019
26e2ae1
Update TensorflowTests.cs
mstfbl Oct 2, 2019
94c0423
Update TensorflowTests.cs
mstfbl Oct 3, 2019
109879b
Fixed test case and off-by-one predictedLabel error
mstfbl Oct 3, 2019
6ff4c64
Merge remote-tracking branch 'upstream/master' into Issue_4169
mstfbl Oct 3, 2019
15c4654
Removed comments
mstfbl Oct 3, 2019
0573ef3
Replacing Path.Join with Path.Combine due to build error with Path.Join
mstfbl Oct 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static void Example()

//Load all the original images info
IEnumerable<ImageData> images = LoadImagesFromDirectory(
folder: fullImagesetFolderPath, useFolderNameasLabel: true);
folder: fullImagesetFolderPath, useFolderNameAsLabel: true);

IDataView shuffledFullImagesDataset = mlContext.Data.ShuffleRows(
mlContext.Data.LoadFromEnumerable(images));
Expand All @@ -63,14 +63,14 @@ public static void Example()
// Just by changing/selecting InceptionV3 here instead of
// ResnetV2101 you can try a different architecture/pre-trained
// model.
arch: ImageClassificationEstimator.Architecture.ResnetV2101,
arch: ImageClassificationEstimator.Architecture.ResnetV2101,
epoch: 50,
batchSize: 10,
learningRate: 0.01f,
metricsCallback: (metrics) => Console.WriteLine(metrics),
validationSet: testDataset,
disableEarlyStopping: true);

disableEarlyStopping: true)
.Append(mlContext.Transforms.Conversion.MapKeyToValue(outputColumnName: "PredictedLabel", inputColumnName: "PredictedLabel"));

Console.WriteLine("*** Training the image classification model with " +
"DNN Transfer Learning on top of the selected pre-trained " +
Expand All @@ -84,7 +84,7 @@ public static void Example()
watch.Stop();
long elapsedMs = watch.ElapsedMilliseconds;

Console.WriteLine("Training with transfer learning took: " +
Console.WriteLine("Training with transfer learning took: " +
(elapsedMs / 1000).ToString() + " seconds");

mlContext.Model.Save(trainedModel, shuffledFullImagesDataset.Schema,
Expand All @@ -97,12 +97,8 @@ public static void Example()

EvaluateModel(mlContext, testDataset, loadedModel);

VBuffer<ReadOnlyMemory<char>> keys = default;
loadedModel.GetOutputSchema(schema)["Label"].GetKeyValues(ref keys);

watch = System.Diagnostics.Stopwatch.StartNew();
TrySinglePrediction(fullImagesetFolderPath, mlContext, loadedModel,
keys.DenseValues().ToArray());
TrySinglePrediction(fullImagesetFolderPath, mlContext, loadedModel);

watch.Stop();
elapsedMs = watch.ElapsedMilliseconds;
Expand All @@ -120,8 +116,7 @@ public static void Example()
}

private static void TrySinglePrediction(string imagesForPredictions,
MLContext mlContext, ITransformer trainedModel,
ReadOnlyMemory<char>[] originalLabels)
MLContext mlContext, ITransformer trainedModel)
{
// Create prediction function to try one prediction
var predictionEngine = mlContext.Model
Expand All @@ -136,12 +131,11 @@ private static void TrySinglePrediction(string imagesForPredictions,
};

var prediction = predictionEngine.Predict(imageToPredict);
var index = prediction.PredictedLabel;

Console.WriteLine($"ImageFile : " +
$"[{Path.GetFileName(imageToPredict.ImagePath)}], " +
$"Scores : [{string.Join(",", prediction.Score)}], " +
$"Predicted Label : {originalLabels[index]}");
$"Predicted Label : {prediction.PredictedLabel}");
}


Expand All @@ -168,7 +162,7 @@ private static void EvaluateModel(MLContext mlContext,
}

public static IEnumerable<ImageData> LoadImagesFromDirectory(string folder,
bool useFolderNameasLabel = true)
bool useFolderNameAsLabel = true)
{
var files = Directory.GetFiles(folder, "*",
searchOption: SearchOption.AllDirectories);
Expand All @@ -179,7 +173,7 @@ public static IEnumerable<ImageData> LoadImagesFromDirectory(string folder,
continue;

var label = Path.GetFileName(file);
if (useFolderNameasLabel)
if (useFolderNameAsLabel)
label = Directory.GetParent(file).Name;
else
{
Expand Down Expand Up @@ -301,7 +295,7 @@ public class ImagePrediction
public float[] Score;

[ColumnName("PredictedLabel")]
public UInt32 PredictedLabel;
public string PredictedLabel;
}
}
}
Expand Down
113 changes: 96 additions & 17 deletions src/Microsoft.ML.Dnn/ImageClassificationTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public sealed class ImageClassificationTransformer : RowToRowTransformerBase
private Graph Graph => _session.graph;
private readonly string[] _inputs;
private readonly string[] _outputs;
private ReadOnlyMemory<char>[] _keyValueAnnotations;
private readonly string _labelColumnName;
private readonly string _finalModelPrefix;
private readonly Architecture _arch;
Expand Down Expand Up @@ -105,11 +106,24 @@ private static ImageClassificationTransformer Create(IHostEnvironment env, Model
// int: number of output columns
// for each output column
// int: id of output column name
// stream: tensorFlow model.
// string: value of label column name
// string: prefix pf final model and checkpoint files/folder for storing graph files
// int: value of the utilized model architecture for transfer learning
// string: value of score column name
// string: value of predicted label column name
// float: value of learning rate
// int: number of prediction classes
// for each key value annotation column
// string: value of key value annotations
// string: name of prediction tensor
// string: name of softmax tensor
// string: name of JPEG data tensor
// string: name of resized image tensor
// stream (byte): tensorFlow model.

GetModelInfo(env, ctx, out string[] inputs, out string[] outputs, out bool addBatchDimensionInput,
out string labelColumn, out string checkpointName, out Architecture arch, out string scoreColumnName,
out string predictedColumnName, out float learningRate, out int classCount, out string predictionTensorName, out string softMaxTensorName,
out string predictedColumnName, out float learningRate, out int classCount, out string[] keyValueAnnotations, out string predictionTensorName, out string softMaxTensorName,
out string jpegDataTensorName, out string resizeTensorName);

byte[] modelBytes = null;
Expand All @@ -119,7 +133,7 @@ private static ImageClassificationTransformer Create(IHostEnvironment env, Model
return new ImageClassificationTransformer(env, DnnUtils.LoadTFSession(env, modelBytes), outputs, inputs,
null, addBatchDimensionInput, 1, labelColumn, checkpointName, arch,
scoreColumnName, predictedColumnName, learningRate, null, classCount, true, predictionTensorName,
softMaxTensorName, jpegDataTensorName, resizeTensorName);
softMaxTensorName, jpegDataTensorName, resizeTensorName, keyValueAnnotations);

}

Expand Down Expand Up @@ -628,7 +642,7 @@ private static IRowMapper Create(IHostEnvironment env, ModelLoadContext ctx, Dat
private static void GetModelInfo(IHostEnvironment env, ModelLoadContext ctx, out string[] inputs,
out string[] outputs, out bool addBatchDimensionInput,
out string labelColumn, out string checkpointName, out Architecture arch,
out string scoreColumnName, out string predictedColumnName, out float learningRate, out int classCount, out string predictionTensorName, out string softMaxTensorName,
out string scoreColumnName, out string predictedColumnName, out float learningRate, out int classCount, out string[] keyValueAnnotations, out string predictionTensorName, out string softMaxTensorName,
out string jpegDataTensorName, out string resizeTensorName)
{
addBatchDimensionInput = ctx.Reader.ReadBoolByte();
Expand All @@ -652,6 +666,12 @@ private static void GetModelInfo(IHostEnvironment env, ModelLoadContext ctx, out
predictedColumnName = ctx.Reader.ReadString();
learningRate = ctx.Reader.ReadFloat();
classCount = ctx.Reader.ReadInt32();

env.CheckDecode(classCount > 0);
keyValueAnnotations = new string[classCount];
for (int j = 0; j < keyValueAnnotations.Length; j++)
keyValueAnnotations[j] = ctx.LoadNonEmptyString();

predictionTensorName = ctx.Reader.ReadString();
softMaxTensorName = ctx.Reader.ReadString();
jpegDataTensorName = ctx.Reader.ReadString();
Expand All @@ -662,7 +682,7 @@ internal ImageClassificationTransformer(IHostEnvironment env, Session session, s
string[] inputColumnNames, string modelLocation,
bool? addBatchDimensionInput, int batchSize, string labelColumnName, string finalModelPrefix, Architecture arch,
string scoreColumnName, string predictedLabelColumnName, float learningRate, DataViewSchema inputSchema, int? classCount = null, bool loadModel = false,
string predictionTensorName = null, string softMaxTensorName = null, string jpegDataTensorName = null, string resizeTensorName = null)
string predictionTensorName = null, string softMaxTensorName = null, string jpegDataTensorName = null, string resizeTensorName = null, string[] labelAnnotations = null)
: base(Contracts.CheckRef(env, nameof(env)).Register(nameof(ImageClassificationTransformer)))

{
Expand Down Expand Up @@ -731,13 +751,55 @@ internal ImageClassificationTransformer(IHostEnvironment env, Session session, s
(_evaluationStep, _) = AddEvaluationStep(_softMaxTensor, _labelTensor);
_softmaxTensorName = _softMaxTensor.name;
_predictionTensorName = _prediction.name;

// Add annotations as key values, if they exist.
VBuffer<ReadOnlyMemory<char>> keysVBuffer = default;
if (inputSchema[labelColumnName].HasKeyValues())
{
inputSchema[labelColumnName].GetKeyValues(ref keysVBuffer);
_keyValueAnnotations = keysVBuffer.DenseValues().ToArray();
}
Copy link
Member

@codemzs codemzs Oct 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it does not have key values then just add numeric values (0 - classCount -1) as string to have something in there #Resolved

else
{
_keyValueAnnotations = Enumerable.Range(0, _classCount).Select(x => x.ToString().AsMemory()).ToArray();
}
Copy link
Member

@codemzs codemzs Oct 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enumerable.Range(0, _classCount).Select(x => x.ToString().AsMemory()).ToArray(); #Resolved

}
else
Copy link

@yaeldekel yaeldekel Sep 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else [](start = 12, length = 4)

I am assuming that this is meant for the case where you are loading the model from file. In that case, you need to use the key values that are deserialized from the model.

In general, I think the way the constructors for this class work is kind of confusing - I think there should be one constructor that is given all the information it needs, either from the deserialization Create method, or from the estimator Fit method. So everything inside if (loadModel == false) would be done in the estimator, as well as figuring out the class count. And the Create method for the SignatureDataTransform should not create a transformer directly, but instead create an estimator, then call Fit and Transform.
#WontFix

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please open a separate issue for making two constructors, this suggestion is out of the scope of this change.


In reply to: 328535147 [](ancestors = 328535147)

{
// Load annotations as key values, if they exist
if (labelAnnotations != null)
_keyValueAnnotations = labelAnnotations.Select(v => v.AsMemory()).ToArray();
}
}

private protected override IRowMapper MakeRowMapper(DataViewSchema inputSchema) => new Mapper(this, inputSchema);

private protected override void SaveModel(ModelSaveContext ctx)
{
// *** Binary format ***
// byte: indicator for frozen models
// byte: indicator for adding batch dimension in input
// int: number of input columns
// for each input column
// int: id of int column name
// int: number of output columns
// for each output column
// int: id of output column name
// string: value of label column name
// string: prefix pf final model and checkpoint files/folder for storing graph files
// int: value of the utilized model architecture for transfer learning
// string: value of score column name
// string: value of predicted label column name
// float: value of learning rate
// int: number of prediction classes
// for each key value annotation column
// string: value of key value annotations
// string: name of prediction tensor
// string: name of softmax tensor
// string: name of JPEG data tensor
// string: name of resized image tensor
// stream (byte): tensorFlow model.

Host.AssertValue(ctx);
ctx.CheckAtModel();
ctx.SetVersionInfo(GetVersionInfo());
Expand All @@ -760,6 +822,12 @@ private protected override void SaveModel(ModelSaveContext ctx)
ctx.Writer.Write(_predictedLabelColumnName);
ctx.Writer.Write(_learningRate);
ctx.Writer.Write(_classCount);

Host.AssertNonEmpty(_keyValueAnnotations);
Copy link

@yaeldekel yaeldekel Oct 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AssertNonEmpty [](start = 17, length = 14)

You cannot assert this, since it is possible that _keyValueAnnotations is null: You are initializing it above in line 750 only if inputSchema[labelColumnName].HasKeyValues(). So you need to serialize a byte indicating whether it is null or not, followed by _classCount strings if it is not null. And same at deserialization time (read a byte, and then read _classCount strings only if that byte is true). #Resolved

Copy link
Contributor Author

@mstfbl mstfbl Oct 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted code with latest push so that _keyValueAnnotations will not be null, thanks to Zeeshan for the suggestion #Resolved

Host.Assert(_keyValueAnnotations.Length == _classCount);
for (int j = 0; j < _classCount; j++)
Copy link
Member

@eerhardt eerhardt Sep 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a minor FYI -

In C#, when you index into an array, the index is bounds-checked at runtime. The runtime checks if the index is non-negative, and less than the length of the array. If it is outside those bounds, an exception is thrown. While these bounds checks aren't very expensive, they can add up.
The C# compiler is smart enough to notice this pattern:

int[] myArray = ...;
for (int i = 0; i < myArray.Length; i++)
{
    int element = myArray[i];
}

It sees this pattern, and knows that i will never be outside the bounds of myArray. So the compiler removes the bounds checks as a performance improvement.

The take away here is, when you have 2 variables to pick from to use in your for loop break condition, and one of them is the .Length of an array you are indexing into, and one isn't. Choose the array.Length variable, so the bounds checks will be removed. #Resolved

Copy link
Contributor Author

@mstfbl mstfbl Sep 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, that makes sense, thank you Eric! It's pretty cool that the C# compiler is able to recognize the traversing of an array and modify the bound checks as it goes. #Resolved

ctx.SaveNonEmptyString(_keyValueAnnotations[j]);
Copy link
Member

@codemzs codemzs Oct 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really guaranteed _keyValueAnnotations is not empty? refer to below lines of code that you have written in the constructor.

VBuffer<ReadOnlyMemory> keysVBuffer = default;
if (inputSchema[labelColumnName].HasKeyValues())
{
inputSchema[labelColumnName].GetKeyValues(ref keysVBuffer);
_keyValueAnnotations = keysVBuffer.DenseValues().ToArray();
} #Resolved

Copy link
Contributor Author

@mstfbl mstfbl Oct 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With your suggested changes below, it is now guaranteed that _keyValueAnnotations will not be empty. #Resolved


ctx.Writer.Write(_predictionTensorName);
ctx.Writer.Write(_softmaxTensorName);
ctx.Writer.Write(_jpegDataTensorName);
Expand Down Expand Up @@ -845,6 +913,7 @@ public void UpdateCacheIfNeeded()
var outputTensor = _runner.AddInput(processedTensor, 0).Run();
outputTensor[0].ToArray<float>(ref _classProbability);
outputTensor[1].ToScalar<long>(ref _predictedLabel);
_predictedLabel += 1;
outputTensor[0].Dispose();
outputTensor[1].Dispose();
processedTensor.Dispose();
Expand Down Expand Up @@ -890,9 +959,18 @@ private protected override Func<int, bool> GetDependenciesCore(Func<int, bool> a

protected override DataViewSchema.DetachedColumn[] GetOutputColumnsCore()
{
var annotationBuilder = new DataViewSchema.Annotations.Builder();
annotationBuilder.AddKeyValues(_parent._classCount, TextDataViewType.Instance, (ref VBuffer<ReadOnlyMemory<char>> dst) =>
{
var editor = VBufferEditor.Create(ref dst, _parent._classCount);
for (int i = 0; i < _parent._classCount; i++)
editor.Values[i] = _parent._keyValueAnnotations[i];
dst = editor.Commit();
});

var info = new DataViewSchema.DetachedColumn[_parent._outputs.Length];
info[0] = new DataViewSchema.DetachedColumn(_parent._outputs[0], new VectorDataViewType(NumberDataViewType.Single, _parent._classCount), null);
info[1] = new DataViewSchema.DetachedColumn(_parent._outputs[1], NumberDataViewType.UInt32, null);
info[0] = new DataViewSchema.DetachedColumn(_parent._scoreColumnName, new VectorDataViewType(NumberDataViewType.Single, _parent._classCount), null);
info[1] = new DataViewSchema.DetachedColumn(_parent._predictedLabelColumnName, new KeyDataViewType(typeof(uint), _parent._classCount), annotationBuilder.ToAnnotations());
return info;
}
}
Expand Down Expand Up @@ -1288,7 +1366,6 @@ internal sealed class Options : TransformInputBase
private readonly Options _options;
private readonly DnnModel _dnnModel;
private readonly TF_DataType[] _tfInputTypes;
private readonly DataViewType[] _outputTypes;
private ImageClassificationTransformer _transformer;

internal ImageClassificationEstimator(IHostEnvironment env, Options options, DnnModel dnnModel)
Expand All @@ -1297,7 +1374,6 @@ internal ImageClassificationEstimator(IHostEnvironment env, Options options, Dnn
_options = options;
_dnnModel = dnnModel;
_tfInputTypes = new[] { TF_DataType.TF_STRING };
_outputTypes = new[] { new VectorDataViewType(NumberDataViewType.Single), NumberDataViewType.UInt32.GetItemType() };
}

private static Options CreateArguments(DnnModel tensorFlowModel, string[] outputColumnNames, string[] inputColumnName, bool addBatchDimensionInput)
Expand Down Expand Up @@ -1327,12 +1403,16 @@ public SchemaShape GetOutputSchema(SchemaShape inputSchema)
if (col.ItemType != expectedType)
throw _host.ExceptSchemaMismatch(nameof(inputSchema), "input", input, expectedType.ToString(), col.ItemType.ToString());
}
for (var i = 0; i < _options.OutputColumns.Length; i++)
{
resultDic[_options.OutputColumns[i]] = new SchemaShape.Column(_options.OutputColumns[i],
_outputTypes[i].IsKnownSizeVector() ? SchemaShape.Column.VectorKind.Vector
: SchemaShape.Column.VectorKind.VariableVector, _outputTypes[i].GetItemType(), false);
}

resultDic[_options.OutputColumns[0]] = new SchemaShape.Column(_options.OutputColumns[0],
SchemaShape.Column.VectorKind.Vector, NumberDataViewType.Single, false);

var metadata = new List<SchemaShape.Column>();
metadata.Add(new SchemaShape.Column(AnnotationUtils.Kinds.KeyValues, SchemaShape.Column.VectorKind.Vector, TextDataViewType.Instance, false));

resultDic[_options.OutputColumns[1]] = new SchemaShape.Column(_options.OutputColumns[1],
SchemaShape.Column.VectorKind.Scalar, NumberDataViewType.UInt32, true, new SchemaShape(metadata.ToArray()));

return new SchemaShape(resultDic.Values);
}

Expand All @@ -1342,8 +1422,7 @@ public SchemaShape GetOutputSchema(SchemaShape inputSchema)
public ImageClassificationTransformer Fit(IDataView input)
{
_host.CheckValue(input, nameof(input));
if (_transformer == null)
_transformer = new ImageClassificationTransformer(_host, _options, _dnnModel, input);
_transformer = new ImageClassificationTransformer(_host, _options, _dnnModel, input);

// Validate input schema.
_transformer.GetOutputSchema(input.Schema);
Expand Down
Loading