Integrate vadimma/CTC into master

This commit is contained in:
Project Philly 2017-01-12 20:36:50 -08:00
Родитель 17d2b04ce9 9deeb31a1e
Коммит 3063238829
11 изменённых файлов: 373 добавлений и 4 удалений

Просмотреть файл

@ -1132,6 +1132,7 @@ UNITTEST_NETWORK_SRC = \
$(SOURCEDIR)/../Tests/UnitTests/NetworkTests/OperatorEvaluation.cpp \
$(SOURCEDIR)/../Tests/UnitTests/NetworkTests/stdafx.cpp \
$(SOURCEDIR)/../Tests/UnitTests/NetworkTests/TestHelpers.cpp \
$(SOURCEDIR)/../Tests/UnitTests/NetworkTests/EditDistanceTests.cpp \
$(SOURCEDIR)/CNTK/ModelEditLanguage.cpp \
$(SOURCEDIR)/ActionsLib/TrainActions.cpp \
$(SOURCEDIR)/ActionsLib/EvalActions.cpp \

Просмотреть файл

@ -160,6 +160,7 @@ bool CheckFunction(std::string& p_nodeType, bool* allowUndeterminedVariable)
#endif
else if (EqualInsensitive(nodeType, OperationNameOf(ClassBasedCrossEntropyWithSoftmaxNode), L"CBCEWithSM")) ret = true;
else if (EqualInsensitive(nodeType, OperationNameOf(ClassificationErrorNode), L"ErrorPrediction")) ret = true;
else if (EqualInsensitive(nodeType, OperationNameOf(EditDistanceErrorNode))) ret = true;
else if (EqualInsensitive(nodeType, OperationNameOf(EqualNode))) ret = true;
else if (EqualInsensitive(nodeType, OperationNameOf(GreaterEqualNode))) ret = true;
else if (EqualInsensitive(nodeType, OperationNameOf(GreaterNode))) ret = true;

Просмотреть файл

@ -294,6 +294,13 @@ public:
return GetNumTimeSteps() * GetNumParallelSequences();
}
// Get the number of frames of the input sequence that belong to the MB, i.e. disregarding sequence elements that are outside of the MB boundaries
// Input sequence is expected to belong to this MBLayout
size_t GetNumSequenceFramesInCurrentMB(const SequenceInfo& sequenceInfo) const
{
return min(sequenceInfo.tEnd, GetNumTimeSteps()) - max(sequenceInfo.tBegin, (ptrdiff_t)0);
}
// return all sequences stored in this minibatch
const vector<SequenceInfo>& GetAllSequences() const
{
@ -501,6 +508,18 @@ public:
return col;
}
// get the matrix-column indices for a given sequence
// sequence is expected to belong to this MB
vector<size_t> GetColumnIndices(const SequenceInfo& seq) const
{
size_t numFrames = GetNumSequenceFramesInCurrentMB(seq);
vector<size_t> res;
res.reserve(numFrames);
for (size_t i = 0; i < numFrames;++i)
res.push_back(GetColumnIndex(seq,i));
return res;
}
private:
// we are trying to access content--this verifies that the structure is consistent
// All frames must now be declared.
@ -822,7 +841,7 @@ inline bool MBLayout::IsBeyondMinibatch(const FrameRange& fr) const
if (fr.IsAllFrames())
LogicError("MBLayout::IsBeyondStartOrEnd() cannot be applied to FrameRange that specifies more than a single time step.");
const auto beginTime = (ptrdiff_t)fr.timeIdxInSeq + fr.m_timeOffset; // we test off the frame without offset
const auto beginTime = (ptrdiff_t)fr.timeIdxInSeq + fr.m_timeOffset; // we test off the frame with offset
const auto endTime = beginTime + (ptrdiff_t)fr.m_timeRange;
return beginTime < 0 || endTime > (ptrdiff_t)GetNumTimeSteps();
}

Просмотреть файл

@ -446,6 +446,7 @@ bool ComputationNetwork::IsTypicalCriterionNode(ComputationNodeBasePtr nodePtr)
nodePtr->OperationName() == OperationNameOf(CrossEntropyNode) ||
nodePtr->OperationName() == OperationNameOf(ClassBasedCrossEntropyWithSoftmaxNode) ||
nodePtr->OperationName() == OperationNameOf(ClassificationErrorNode) ||
nodePtr->OperationName() == OperationNameOf(EditDistanceErrorNode) ||
#ifdef COMING_SOON
nodePtr->OperationName() == OperationNameOf(CRFNode) ||
#endif

Просмотреть файл

@ -54,6 +54,7 @@ static shared_ptr<ComputationNode<ElemType>> CreateStandardNode(const std::wstri
else if (nodeType == OperationNameOf(DropoutNode)) return New<DropoutNode<ElemType>>(forward<_Types>(_Args)...);
else if (nodeType == OperationNameOf(DummyCriterionNode)) return New<DummyCriterionNode<ElemType>>(forward<_Types>(_Args)...);
else if (nodeType == OperationNameOf(DynamicAxisNode)) return New<DynamicAxisNode<ElemType>>(forward<_Types>(_Args)...);
else if (nodeType == OperationNameOf(EditDistanceErrorNode)) return New<EditDistanceErrorNode<ElemType>>(forward<_Types>(_Args)...);
else if (nodeType == OperationNameOf(ElementTimesNode)) return New<ElementTimesNode<ElemType>>(forward<_Types>(_Args)...);
else if (nodeType == OperationNameOf(EnvironmentInputNode)) return New<EnvironmentInputNode<ElemType>>(forward<_Types>(_Args)...);
else if (nodeType == OperationNameOf(EpochAccumulatorNode)) return New<EpochAccumulatorNode<ElemType>>(forward<_Types>(_Args)...);
@ -427,6 +428,12 @@ shared_ptr<ComputationNode<ElemType>> ComputationNetworkBuilder<ElemType>::Class
return net.AddNodeToNetAndAttachInputs(New<ClassificationErrorNode<ElemType>>(net.GetDeviceId(), nodeName), { a, b });
}
template <class ElemType>
shared_ptr<ComputationNode<ElemType>> ComputationNetworkBuilder<ElemType>::EditDistanceError(const ComputationNodePtr a, const ComputationNodePtr b, float subPen, float delPen, float insPen, bool squashInputs, vector<int> samplesToIgnore, const std::wstring nodeName)
{
return net.AddNodeToNetAndAttachInputs(New<EditDistanceErrorNode<ElemType>>(net.GetDeviceId(), nodeName, subPen, delPen, insPen, squashInputs, samplesToIgnore), { a, b });
}
template <class ElemType>
shared_ptr<ComputationNode<ElemType>> ComputationNetworkBuilder<ElemType>::PerDimMeanVarNormalization(const ComputationNodePtr feature, const ComputationNodePtr mean,
const ComputationNodePtr InvStdDev, const std::wstring nodeName)

Просмотреть файл

@ -129,6 +129,7 @@ public:
ComputationNodePtr Diagonal(const ComputationNodePtr a, const std::wstring nodeName = L"");
ComputationNodePtr Dropout(const ComputationNodePtr a, const std::wstring nodeName = L"");
ComputationNodePtr DummyCriterion(const ComputationNodePtr objectives, const ComputationNodePtr derivatives, const ComputationNodePtr prediction, const std::wstring nodeName = L"");
ComputationNodePtr EditDistanceError(const ComputationNodePtr a, const ComputationNodePtr b, float subPen, float delPen, float insPen, bool squashInputs, vector<int> samplesToIgnore, const std::wstring nodeName = L"");
ComputationNodePtr ElementTimes(const ComputationNodePtr a, const ComputationNodePtr b, const std::wstring nodeName = L"");
ComputationNodePtr DynamicAxis(const ComputationNodePtr a, const std::wstring& nodeName = L"");
ComputationNodePtr ClassificationError(const ComputationNodePtr a, const ComputationNodePtr b, const std::wstring nodeName = L"");

Просмотреть файл

@ -7,7 +7,8 @@
#include "Basics.h"
#include "ComputationNode.h"
#include "gammacalculation.h"
#include "InputAndParamNodes.h"
#include "Sequences.h"
#include <map>
#include <string>
#include <vector>
@ -15,6 +16,7 @@
#include <list>
#include <memory>
namespace Microsoft { namespace MSR { namespace CNTK {
// -----------------------------------------------------------------------
@ -456,6 +458,287 @@ protected:
template class NDCG1EvalNode<float>;
template class NDCG1EvalNode<double>;
// Edit distance error evaluation node with the option of specifying penalty of substitution, deletion and insertion, as well as squashing the input sequences and ignoring certain samples.
// Using the classic DP algorithm as described in https://en.wikipedia.org/wiki/Edit_distance, adjusted to take into account the penalties.
//
// The node allows to squash sequences of repeating labels and ignore certain labels. For example, if squashInputs is true and samplesToIgnore contains label '-' then
// given first input sequence as s1="a-ab-" and second as s2="-aa--abb" the edit distance will be computed against s1' = "aab" and s2' = "aab".
//
// The returned error is computed as: EditDistance(s1,s2) * length(s1') / length(s1)
//
// Just like ClassificationError and other evaluation nodes, when used as an evaluation criterion, the SGD process will aggregate all values over an epoch and report the average, i.e. the error rate.
// Primary objective of this node is for error evaluation of CTC training, see formula (1) in "Connectionist Temporal Classification: Labelling Unsegmented
// Sequence Data with Recurrent Neural Networks", http://machinelearning.wustl.edu/mlpapers/paper_files/icml2006_GravesFGS06.pdf
template<class ElemType>
class EditDistanceErrorNode : public ComputationNodeNonLooping/*ComputationNode*/<ElemType>, public NumInputs<2>
{
typedef ComputationNodeNonLooping<ElemType> Base; UsingComputationNodeMembersBoilerplate;
static const std::wstring TypeName() { return L"EditDistanceError"; }
public:
// subPen - substitution penalty
// delPen - deletion penalty
// insPen - insertion penalty
// squashInputs - whether to merge sequences of identical samples.
// samplesToIgnore - list of samples to ignore during edit distance evaluation
EditDistanceErrorNode(DEVICEID_TYPE deviceId, const wstring & name, float subPen, float delPen, float insPen, bool squashInputs, vector<int> samplesToIgnore)
: Base(deviceId, name), m_SubPen(subPen), m_DelPen(delPen), m_InsPen(insPen), m_SquashInputs(squashInputs), m_SamplesToIgnore(samplesToIgnore)
{
}
EditDistanceErrorNode(const ScriptableObjects::IConfigRecordPtr configp)
: EditDistanceErrorNode(configp->Get(L"deviceId"), L"<placeholder>", configp->Get(L"subPen"), configp->Get(L"delPen"), configp->Get(L"insPen"), configp->Get(L"squashInputs"), configp->Get(L"samplesToIgnore"))
{
AttachInputsFromConfig(configp, this->GetExpectedNumInputs());
}
EditDistanceErrorNode(DEVICEID_TYPE deviceId, const wstring& name)
: Base(deviceId, name)
{
}
virtual void BackpropToNonLooping(size_t /*inputIndex*/) override
{
LogicError("%ls operation is used for evaluation only.", OperationName().c_str());
}
virtual void ForwardPropNonLooping() override
{
bool isInput0Sparse = Input(0)->template Is<SparseInputValue<ElemType>>();
bool isInput1Sparse = Input(1)->template Is<SparseInputValue<ElemType>>();
if (isInput0Sparse || isInput1Sparse)
LogicError("EditDistanceError node was not tested for sparse inputs.");
FrameRange frameRange(Input(0)->GetMBLayout());
Input(0)->ValueFor(frameRange).VectorMax(*m_maxIndexes0, *m_maxValues, true);
Input(1)->ValueFor(frameRange).VectorMax(*m_maxIndexes1, *m_maxValues, true);
MaskMissingColumnsToZero(*m_maxIndexes0, Input(0)->GetMBLayout(), frameRange);
MaskMissingColumnsToZero(*m_maxIndexes1, Input(1)->GetMBLayout(), frameRange);
Value()(0, 0) = ComputeEditDistanceError(*m_maxIndexes0, *m_maxIndexes1, Input(0)->GetMBLayout(), m_SubPen, m_DelPen, m_InsPen, m_SquashInputs, m_SamplesToIgnore);
}
virtual void Validate(bool isFinalValidationPass) override
{
ValidateBinaryReduce(isFinalValidationPass);
}
virtual void UpdateFunctionMBSize() override
{
Base::UpdateFunctionMBSize();
// resize the temporaries to their proper size
size_t cols = Input(0)->Value().GetNumCols();
m_maxIndexes0->Resize(1, cols);
m_maxIndexes1->Resize(1, cols);
m_maxValues->Resize(1, cols);
}
virtual void CopyTo(ComputationNodeBasePtr nodeP, const std::wstring& newName, const CopyNodeFlags flags) const override
{
Base::CopyTo(nodeP, newName, flags);
if (flags & CopyNodeFlags::copyNodeValue)
{
auto node = dynamic_pointer_cast<EditDistanceErrorNode<ElemType>>(nodeP);
node->m_maxIndexes0 = m_maxIndexes0;
node->m_maxIndexes1 = m_maxIndexes1;
node->m_maxValues = m_maxValues;
node->m_SquashInputs = m_SquashInputs;
node->m_SubPen = m_SubPen;
node->m_DelPen = m_DelPen;
node->m_InsPen = m_InsPen;
node->m_SamplesToIgnore = m_SamplesToIgnore;
}
}
//request matrices needed to do node function value evaluation
virtual void RequestMatricesBeforeForwardProp(MatrixPool& matrixPool)
{
Base::RequestMatricesBeforeForwardProp(matrixPool);
RequestMatrixFromPool(m_maxIndexes0, matrixPool);
RequestMatrixFromPool(m_maxIndexes1, matrixPool);
RequestMatrixFromPool(m_maxValues, matrixPool);
}
//release temp matrices that are only used by forward computation
//don't release matrices that need to be used in the gradient computation
virtual void ReleaseMatricesAfterForwardProp(MatrixPool& matrixPool)
{
Base::ReleaseMatricesAfterForwardProp(matrixPool);
ReleaseMatrixToPool(m_maxIndexes0, matrixPool);
ReleaseMatrixToPool(m_maxIndexes1, matrixPool);
ReleaseMatrixToPool(m_maxValues, matrixPool);
}
// firstSeq - first sequence of samples
// secondSeq - second sequence of samples
// numParallelSequences - number of parallel sequences in the minibatch
// subPen - substitution penalty
// delPen - deletion penalty
// insPen - insertion penalty
// squashInputs - whether to merge sequences of identical samples.
// samplesToIgnore - list of samples to ignore during edit distance evaluation
static ElemType ComputeEditDistanceError(Matrix<ElemType>& firstSeq, const Matrix<ElemType> & secondSeq, MBLayoutPtr pMBLayout,
float subPen, float delPen, float insPen, bool squashInputs, const vector<int>& samplesToIgnore)
{
std::vector<int> firstSeqVec, secondSeqVec;
// Edit distance between subsequences
Matrix<float> grid(CPUDEVICE);
// Number of insertions between subsequences
Matrix<float> insMatrix(CPUDEVICE);
//Number of deletions between subsequences
Matrix<float> delMatrix(CPUDEVICE);
// Number of substitutions between subsequences
Matrix<float> subMatrix(CPUDEVICE);
float del, ins, sub;
ElemType wrongSampleNum = 0.0;
size_t totalSampleNum = 0, totalframeNum = 0;
size_t sequenceStartFrame = 0;
for (const auto& sequence : pMBLayout->GetAllSequences())
{
if (sequence.seqId == GAP_SEQUENCE_ID)
continue;
auto numFrames = pMBLayout->GetNumSequenceFramesInCurrentMB(sequence);
if (numFrames > 0)
{
totalframeNum += numFrames;
auto columnIndices = pMBLayout->GetColumnIndices(sequence);
ExtractSampleSequence(firstSeq, columnIndices, squashInputs, samplesToIgnore, firstSeqVec);
ExtractSampleSequence(secondSeq, columnIndices, squashInputs, samplesToIgnore, secondSeqVec);
//calculate edit distance
size_t firstSize = firstSeqVec.size();
totalSampleNum += firstSize;
size_t secondSize = secondSeqVec.size();
grid.Resize(firstSize + 1, secondSize + 1);
insMatrix.Resize(firstSize + 1, secondSize + 1);
delMatrix.Resize(firstSize + 1, secondSize + 1);
subMatrix.Resize(firstSize + 1, secondSize + 1);
insMatrix.SetValue(0.0f);
delMatrix.SetValue(0.0f);
subMatrix.SetValue(0.0f);
for (size_t i = 0; i < firstSize + 1; i++)
{
grid(i, 0) = (float)(i * delPen);
delMatrix(i, 0) = (float)i;
}
for (size_t j = 0; j < secondSize + 1; j++)
{
grid(0, j) = (float)(j * insPen);
insMatrix(0, j) = (float)j;
}
for (size_t i = 1; i < firstSize + 1; i++)
{
for (size_t j = 1; j < secondSize + 1; j++)
{
if (firstSeqVec[i - 1] == secondSeqVec[j - 1])
{
grid(i, j) = grid(i - 1, j - 1);
insMatrix(i, j) = insMatrix(i - 1, j - 1);
delMatrix(i, j) = delMatrix(i - 1, j - 1);
subMatrix(i, j) = subMatrix(i - 1, j - 1);
}
else
{
del = grid(i - 1, j) + delPen; //deletion
ins = grid(i, j - 1) + insPen; //insertion
sub = grid(i - 1, j - 1) + subPen; //substitution
if (sub <= del && sub <= ins)
{
insMatrix(i, j) = insMatrix(i - 1, j - 1);
delMatrix(i, j) = delMatrix(i - 1, j - 1);
subMatrix(i, j) = subMatrix(i - 1, j - 1) + 1.0f;
grid(i, j) = sub;
}
else if (del < ins)
{
insMatrix(i, j) = insMatrix(i - 1, j);
subMatrix(i, j) = subMatrix(i - 1, j);
delMatrix(i, j) = delMatrix(i - 1, j) + 1.0f;
grid(i, j) = del;
}
else
{
delMatrix(i, j) = delMatrix(i, j - 1);
subMatrix(i, j) = subMatrix(i, j - 1);
insMatrix(i, j) = insMatrix(i, j - 1) + 1.0f;
grid(i, j) = ins;
}
}
}
}
wrongSampleNum += insMatrix(firstSize, secondSize) + delMatrix(firstSize, secondSize) + subMatrix(firstSize, secondSize);
}
sequenceStartFrame += numFrames;
}
return (ElemType)(wrongSampleNum * totalframeNum / totalSampleNum);
}
private:
shared_ptr<Matrix<ElemType>> m_maxIndexes0, m_maxIndexes1;
shared_ptr<Matrix<ElemType>> m_maxValues;
bool m_SquashInputs;
float m_SubPen;
float m_DelPen;
float m_InsPen;
std::vector<int> m_SamplesToIgnore;
// Clear out_SampleSeqVec and extract a vector of samples from the matrix into out_SampleSeqVec.
static void ExtractSampleSequence(const Matrix<ElemType>& firstSeq, vector<size_t>& columnIndices, bool squashInputs, const vector<int>& samplesToIgnore, std::vector<int>& out_SampleSeqVec)
{
out_SampleSeqVec.clear();
// Get the first element in the sequence
size_t lastId = (int)firstSeq(0, columnIndices[0]);
if (std::find(samplesToIgnore.begin(), samplesToIgnore.end(), lastId) == samplesToIgnore.end())
out_SampleSeqVec.push_back(lastId);
// Remaining elements
if (squashInputs)
{
//squash sequences of identical samples
for (size_t i = 1; i < columnIndices.size(); i++)
{
size_t refId = (int)firstSeq(0, columnIndices[i]);
if (lastId != refId)
{
lastId = refId;
if (std::find(samplesToIgnore.begin(), samplesToIgnore.end(), refId) == samplesToIgnore.end())
out_SampleSeqVec.push_back(refId);
}
}
}
else
{
for (size_t i = 1; i < columnIndices.size(); i++)
{
auto refId = (int)firstSeq(0, columnIndices[i]);
if (std::find(samplesToIgnore.begin(), samplesToIgnore.end(), refId) == samplesToIgnore.end())
out_SampleSeqVec.push_back(refId);
}
}
}
};
template class EditDistanceErrorNode<float>;
template class EditDistanceErrorNode<double>;
#ifdef COMING_SOON
// -----------------------------------------------------------------------

Просмотреть файл

@ -54,6 +54,12 @@ Test module "NetworkTests" has passed with:
Test case "CropNodeTestSuite/CropNodeBackwardTest" has passed with:
40 assertions out of 40 passed
Test suite "EditDistanceTests" has passed with:
1 test cases out of 1 passed
1 assertions out of 1 passed
Test case "EditDistanceTests/ComputeEditDistanceErrorTest" has passed
Test suite "NetworkTestSuite" has passed with:
1 test case out of 1 passed
1 assertion out of 1 passed

Просмотреть файл

@ -0,0 +1,48 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
//
#include "stdafx.h"
#include "EvaluationNodes.h"
using namespace Microsoft::MSR::CNTK;
namespace Microsoft { namespace MSR { namespace CNTK { namespace Test {
BOOST_AUTO_TEST_SUITE(EditDistanceTests)
BOOST_AUTO_TEST_CASE(ComputeEditDistanceErrorTest)
{
Matrix<float> firstSeq(CPUDEVICE);
Matrix<float> secondSeq(CPUDEVICE);
vector<int> samplesToIgnore;
size_t seqSize = 10;
firstSeq.Resize(1, seqSize);
secondSeq.Resize(1, seqSize);
for (size_t i = 0; i < seqSize; i++)
{
firstSeq(0, i) = (float)i;
secondSeq(0, i) = (float)i - 1;
}
MBLayoutPtr pMBLayout = make_shared<MBLayout>(1, seqSize, L"X");
pMBLayout->AddSequence(0, 0, 0, seqSize);
float ed = EditDistanceErrorNode<float>::ComputeEditDistanceError(firstSeq, secondSeq, pMBLayout, 1, 1, 1, true, samplesToIgnore);
assert((int)ed == 2);
for (size_t i = 0; i < seqSize; i++)
{
secondSeq(0, i) = (float)i;
}
ed = EditDistanceErrorNode<float>::ComputeEditDistanceError(firstSeq, secondSeq, pMBLayout, 1, 1, 1, true, samplesToIgnore);
assert((int)ed == 0);
secondSeq(0, seqSize-1) = (float)123;
ed = EditDistanceErrorNode<float>::ComputeEditDistanceError(firstSeq, secondSeq, pMBLayout, 1, 1, 1, true, samplesToIgnore);
assert((int)ed == 1);
}
BOOST_AUTO_TEST_SUITE_END()
} } } }

Просмотреть файл

@ -56,7 +56,7 @@
<PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UseFullPaths>true</UseFullPaths>
<OpenMPSupport>true</OpenMPSupport>
<AdditionalIncludeDirectories>$(MSMPI_INC);$(SolutionDir)Source\Readers\ReaderLib;$(SolutionDir)Source\Common\Include;$(SolutionDir)Source\Math;$(SolutionDir)Source\ActionsLib;$(SolutionDir)Source\ComputationNetworkLib;$(SolutionDir)Source\CNTK\BrainScript;$(BOOST_INCLUDE_PATH)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(MSMPI_INC);$(SolutionDir)Source\Readers\ReaderLib;$(SolutionDir)Source\SequenceTrainingLib;$(SolutionDir)Source\Common\Include;$(SolutionDir)Source\Math;$(SolutionDir)Source\ActionsLib;$(SolutionDir)Source\ComputationNetworkLib;$(SolutionDir)Source\CNTK\BrainScript;$(BOOST_INCLUDE_PATH)</AdditionalIncludeDirectories>
<DisableSpecificWarnings>4819</DisableSpecificWarnings>
</ClCompile>
<Link>
@ -112,6 +112,7 @@
<ClCompile Include="..\..\..\Source\CNTK\BrainScript\BrainScriptParser.cpp" />
<ClCompile Include="AccumulatorNodeTests.cpp" />
<ClCompile Include="CropNodeTests.cpp" />
<ClCompile Include="EditDistanceTests.cpp" />
<ClCompile Include="OperatorEvaluation.cpp" />
<ClCompile Include="stdafx.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>

Просмотреть файл

@ -20,6 +20,7 @@
<ClCompile Include="AccumulatorNodeTests.cpp" />
<ClCompile Include="CropNodeTests.cpp" />
<ClCompile Include="TestHelpers.cpp" />
<ClCompile Include="EditDistanceTests.cpp" />
</ItemGroup>
<ItemGroup>
<Filter Include="Config">
@ -49,4 +50,4 @@
<Filter>Config</Filter>
</Text>
</ItemGroup>
</Project>
</Project>