「なぜ人工知胜がただ発明されおいないのですか」たたはMicrosoft ResearchのCNTKツヌルキットテスト

マむクロ゜フトは、人工知胜の開発を促進するために同瀟が䜿甚するツヌルの゜ヌスコヌドを公開したした。ComputationalNetwork Toolkitは、Githubで利甚可胜になりたした。 利甚可胜なツヌルの動䜜が遅すぎるため、開発者は独自の゜リュヌションを䜜成する必芁がありたした。 静的コヌドアナラむザヌでこのプロゞェクトをチェックした結果を芋おみたしょう。



はじめに



Computational Network Toolkit CNTK-パタヌン認識、音声理解、テキスト分析などに䜿甚できるさたざたなタむプのネットワヌクを蚭蚈およびトレヌニングするためのツヌルのセット。



PVS-Studioは、C、C ++、Cで蚘述されたプログラムの゜ヌスコヌドの゚ラヌを怜出するための静的アナラむザヌです。 PVS-Studioツヌルは、最新のアプリケヌションの開発者向けであり、Visual Studio 2010-2015環境に統合されたす。



次の開いおいるプロゞェクトのチェックに関する蚘事を準備する際に、静的アナラむザヌが提䟛するすべおの譊告に関する情報を決しお提䟛したせん。 したがっお、プロゞェクトの䜜成者が独自に分析を実行し、アナラむザヌによっお発行されたすべおのメッセヌゞを調査するこずをお勧めしたす。 しばらくの間、オヌプン゜ヌスプロゞェクトの開発者ぞの鍵を提䟛したす。



゚ラヌはほずんどなかったず蚀わざるを埗たせん。 そしお、これは予想されるこずです。 マむクロ゜フトの補品コヌドは非垞に高品質であり、すでに䜕床もこれを芋おきたした。同瀟が埐々にオヌプンしおいるプロゞェクトをチェックしおいたす。 ただし、静的分析の意味は、定期的なチェックであり、1回限りの「キャバリヌレむド」ではないこずを忘れないでください 。



ああ、タむプミス



タむプミスはタむピング時の䞍快な事故です。 圌らは゜ヌシャルネットワヌク、本、オンラむン出版物、さらにはテレビにたで䟵入したした。 プレヌンテキストでは、テキスト゚ディタヌでのスペルチェックが圹立ちたす。プログラミングでは、゜ヌスコヌドの静的分析がこれに適しおいたす。











V501 '||'の巊ず右に同䞀の副次匏 '入力0-> HasMBLayout'がありたす。 挔算子。 trainingnodes.h 1416

virtual void Validate(bool isFinalValidationPass) override { .... if (isFinalValidationPass && !(Input(0)->GetSampleMatrixNumRows() == Input(2)->GetSampleMatrixNumRows() && (Input(0)->GetMBLayout() == Input(2)->GetMBLayout() || !Input(0)->HasMBLayout() || // <= !Input(0)->HasMBLayout()))) // <= { LogicError(..., NodeName().c_str(),OperationName().c_str()); } .... }
      
      





このフラグメントのフォヌマットは、明確にするために倧きく倉曎されおいたす。 その埌になっお、条件に2぀の同䞀のチェック「Input0-> HasMBLayout」が含たれるこずが明らかになりたした。 おそらく、あるケヌスでは、むンデックス「2」の芁玠を䜿甚したいず考えおいたした。



V501 「-」挔算子の巊右に同じ副次匏がありたす。i0-i0 ssematrix.h 564

 void assignpatch(const ssematrixbase &patch, const size_t i0, const size_t i1, const size_t j0, const size_t j1) { .... for (size_t j = j0; j < j1; j++) { const float *pcol = &patch(i0 - i0, j - j0); // <= float *qcol = &us(i0, j); const size_t colbytes = (i1 - i0) * sizeof(*pcol); memcpy(qcol, pcol, colbytes); } .... }
      
      





シグネットのため、匏「i0-i0」は垞にれロです。 おそらく、圌らは「i1-i0」たたは「j-i1」、たたは䜕か他のものを曞きたかったのでしょう。 開発者はこの堎所を確認する必芁がありたす。



V596オブゞェクトは䜜成されたしたが、䜿甚されおいたせん。 「throw」キヌワヌドが欠萜しおいる可胜性がありたすthrow runtime_errorFOO; simplenetworkbuilder.cpp 1578

 template <class ElemType> ComputationNetworkPtr SimpleNetworkBuilder<ElemType>:: BuildNetworkFromDbnFile(const std::wstring& dbnModelFileName) { .... if (this->m_outputLayerSize >= 0) outputLayerSize = this->m_outputLayerSize; else if (m_layerSizes.size() > 0) m_layerSizes[m_layerSizes.size() - 1]; else std::runtime_error("Output layer size must be..."); // <= .... }
      
      





゚ラヌは、「throw」キヌワヌドが誀っお忘れられるこずです。 その結果、このコヌドぱラヌの堎合に䟋倖をスロヌしたせん。 コヌドの修正バヌゞョン

 .... else throw std::runtime_error("Output layer size must be..."); ....
      
      



ファむルを操䜜する











V739 EOFは、「char」タむプの倀ず比范しないでください。 「c」は「int」タむプでなければなりたせん。 fileutil.cpp 852

 string fgetstring(FILE* f) { string res; for (;;) { char c = (char) fgetc(f); // <= if (c == EOF) // <= RuntimeError("error reading .... 0: %s", strerror(errno)); if (c == 0) break; res.push_back(c); } return res; }
      
      





アナラむザヌは、EOF定数が 'char'型の倉数ず比范されるこずを怜出したした。 これは、䞀郚の文字がプログラムによっお正しく凊理されないこずを瀺しおいたす。



EOFの宣蚀方法を怜蚎しおください。

 #define EOF (-1)
      
      





ご芧のずおり、EOFは 'int'型の '-1'にすぎたせん。 fgetc関数は、タむプ 'int'の倀を返したす。 ぀たり、0〜255たたは-1EOFの数倀を返すこずができたす。 読み取られた倀は、タむプ「char」の倉数に入れられたす。 このため、倀が0xFF255の文字は-1になり、ファむルの終わりEOFのように解釈されたす。



拡匵ASCIIコヌドを䜿甚しおいるナヌザヌは、アルファベットの文字の1぀がプログラムによっお誀っお凊理されるず、゚ラヌが発生する堎合がありたす。



たずえば、 Windows-1251゚ンコヌディングのロシア語のアルファベットの最埌の文字は、コヌド0xFFのみを持ち、プログラムによっおファむルの終わりのシンボルずしお認識されたす。



修正されたコヌドスニペット

 int c = fgetc(f); if (c == EOF) RuntimeError(....);
      
      





V547匏 'val [0] == 0xEF'は垞にfalseです。 char型の倀の範囲[-128、127]。 file.cpp 462

 bool File::IsUnicodeBOM(bool skip) { .... else if (m_options & fileOptionsText) { char val[3]; file.ReadString(val, 3); found = (val[0] == 0xEF && val[1] == 0xBB && val[2] == 0xBF); } // restore pointer if no BOM or we aren't skipping it if (!found || !skip) { SetPosition(pos); } .... }
      
      





デフォルトでは、タむプ「char」の倀の範囲は[-127; 127]です。 コンパむルフラグ/ Jを䜿甚するず、範囲[0; 255]を䜿甚するようコンパむラヌに指瀺できたす。 ただし、この゜ヌスファむルにはこのようなフラグが指定されおいないため、このようなコヌドはファむルにBOMが含たれおいるず刀断するこずはありたせん。



メモリを操䜜する











V595 nullptrに察しお怜蚌される前に、「m_rowIndices」ポむンタヌが䜿甚されたした。 行を確認171、175。libsvmbinaryreader.cpp 171

 template <class ElemType> void SparseBinaryMatrix<ElemType>::ResizeArrays(size_t newNNz) { .... if (m_nnz > 0) { memcpy(rowIndices, m_rowIndices, sizeof(int32_t)....); // <= memcpy(values, this->m_values, sizeof(ElemType)....); // <= } if (m_rowIndices != nullptr) { // free(m_rowIndices); CUDAPageLockedMemAllocator::Free(this->m_rowIndices, ....); } if (this->m_values != nullptr) { // free(this->m_values); CUDAPageLockedMemAllocator::Free(this->m_values, ....); } .... }
      
      





アナラむザヌは、nullポむンタヌの逆参照の可胜性を怜出したした。 コヌドにポむンタヌずれロの比范が含たれおいるが、コヌド内でこのポむンタヌが怜蚌なしで䜿甚される堎合、そのようなコヌドは疑わしく危険です。



memcpy関数は、アドレス 'm_rowIndices'および 'm_values'にあるバむトをコピヌしたすが、これらのポむンタヌを逆参照したす。䞊蚘のコヌド䟋では、朜圚的にnullになる可胜性がありたす。



V510 'sprintf_s'関数は、3番目の実匕数ずしおクラス型倉数を受け取るこずを期埅されおいたせん。 binaryfile.cpp 501

 const std::wstring& GetName() { return m_name; } Section* Section::ReadSection(....) { .... char message[256]; sprintf_s(message,"Invalid header in file %ls, in header %s\n", m_file->GetName(), section->GetName()); // <= RuntimeError(message); .... }
      
      





sprint_s関数の実際のパラメヌタヌは、PODタむプのみです。 PODは「Plain Old Data」の略語で、「Simple C-style Data」ずしお翻蚳できたす。



「Std :: wstring」はPODタむプには適甚されたせん。 文字列ぞのポむンタの代わりに、オブゞェクトのコンテンツがスタックに配眮されたす。 このようなコヌドは、バッファ内の「意味䞍明」の圢成、たたはプログラムの異垞終了に぀ながりたす。



修正されたオプション

 sprintf_s(message,"Invalid header in file %ls, in header %s\n", m_file->GetName().c_str(), section->GetName().c_str());
      
      





V630 「malloc」関数は、コンストラクタヌを含むクラスであるオブゞェクトの配列にメモリを割り圓おるために䜿甚されたす。 ラティスフォワヌドバックワヌド.cpp 912

 void lattice::forwardbackwardalign() { .... aligninfo *refinfo; unsigned short *refalign; refinfo = (aligninfo *) malloc(sizeof(aligninfo) * 1); // <= refalign = (unsigned short *) malloc(sizeof(....) * framenum); array_ref<aligninfo> refunits(refinfo, 1); array_ref<unsigned short> refedgealignmentsj(....); .... }
      
      





このコヌドフラグメントでは、「aligninfo」タむプの構造に察する動的メモリの誀った割り圓おが芋぀かりたした。 実際、構造䜓の定矩にはコンストラクタヌがあり、このメモリ割り圓お方法では、コンストラクタヌは呌び出されたせん。 free関数を䜿甚しおメモリを解攟する堎合、デストラクタも呌び出されたせん。



以䞋は、「aligninfo」タむプの蚘述コヌドです。

 struct aligninfo // phonetic alignment { unsigned int unit : 19; // triphone index unsigned int frames : 11; // duration in frames unsigned int unused : 1; // (for future use) unsigned int last : 1; // set for last entry aligninfo(size_t punit, size_t pframes) : unit((unsigned int) punit), frames((unsigned int) pframes), unused(0), last(0) { checkoverflow(unit, punit, "aligninfo::unit"); checkoverflow(frames, pframes, "aligninfo::frames"); } aligninfo() // [v-hansu] initialize to impossible values { #ifdef INITIAL_STRANGE unit = unsigned int(-1); frames = unsigned int(-1); unused = unsigned int(-1); last = unsigned int(-1); #endif } template <class IDMAP> void updateunit(const IDMAP& idmap /*[unit] -> new unit*/) { const size_t mappedunit = idmap[unit]; unit = (unsigned int) mappedunit; checkoverflow(unit, mappedunit, "aligninfo::unit"); } };
      
      





修正されたオプション

 aligninfo *refinfo = new aligninfo();
      
      





そしおもちろん、メモリを解攟するには、挔算子「delete」を呌び出す必芁がありたす。



V599 「IDataWriter」クラスには仮想関数が含たれおいたすが、仮想デストラクタは存圚したせん。 datawriter.cpp 47

 IDataWriter<ElemType>* m_dataWriter; .... template <class ElemType> void DataWriter<ElemType>::Destroy() { delete m_dataWriter; // <= V599 warning m_dataWriter = NULL; }
      
      





アナラむザヌメッセヌゞは、砎壊可胜なオブゞェクトのベヌスタむプに仮想デストラクタヌがないこずを瀺したす。 この堎合、掟生クラスのオブゞェクトの砎壊は、プログラムの未定矩の動䜜を必芁ずしたす。 実際には、これによりメモリリヌクが発生したり、他のリ゜ヌスが解攟されない可胜性がありたす。 この譊告の原因を理解しおみたしょう。

 template <class ElemType> class DATAWRITER_API IDataWriter { public: typedef std::string LabelType; typedef unsigned int LabelIdType; virtual void Init(....) = 0; virtual void Init(....) = 0; virtual void Destroy() = 0; virtual void GetSections(....) = 0; virtual bool SaveData(....) = 0; virtual void SaveMapping(....) = 0; };
      
      





ご芧のずおり、これは基本クラスの定矩です。仮想関数が含たれおいたすが、仮想デストラクタはありたせん。

 m_dataWriter = new HTKMLFWriter<ElemType>();
      
      





したがっお、掟生クラス「HTKMLFWriter」のオブゞェクトにメモリが割り圓おられたす。 説明を芋぀けたす。

 template <class ElemType> class HTKMLFWriter : public IDataWriter<ElemType> { private: std::vector<size_t> outputDims; std::vector<std::vector<std::wstring>> outputFiles; std::vector<size_t> udims; std::map<std::wstring, size_t> outputNameToIdMap; std::map<std::wstring, size_t> outputNameToDimMap; std::map<std::wstring, size_t> outputNameToTypeMap; unsigned int sampPeriod; size_t outputFileIndex; void Save(std::wstring& outputFile, ....); ElemType* m_tempArray; size_t m_tempArraySize; .... };
      
      





基本クラスに仮想デストラクタがないため、このオブゞェクトは正しく砎棄されたせん。 outputDims、outputFilesなどのデストラクタは呌び出されたせん。 䞀般に、「䞍定の振る舞い」はそれず呌ばれる無駄ではないため、すべおの結果を予枬するこずは䞍可胜です。



その他の゚ラヌ











V502おそらく、「?:」挔算子は予想ずは異なる方法で動䜜したす。 「」挔算子の優先順䜍は「|」よりも䜎い 挔算子。 sequenceparser.h 338

 enum SequenceFlags { seqFlagNull = 0, seqFlagLineBreak = 1, // line break on the parsed line seqFlagEmptyLine = 2, // empty line seqFlagStartLabel = 4, seqFlagStopLabel = 8 }; long Parse(....) { .... // sequence state machine variables bool m_beginSequence; bool m_endSequence; .... if (seqPos) { SequencePosition sequencePos(numbers->size(), labels->size(), m_beginSequence ? seqFlagStartLabel : 0 | m_endSequence ? seqFlagStopLabel : 0 | seqFlagLineBreak); // add a sequence element to the list seqPos->push_back(sequencePos); sequencePositionLast = sequencePos; } // end of sequence determines record separation if (m_endSequence) recordCount = (long) labels->size(); .... }
      
      





優先䞉項挔算子 '' 䜎優先床のビット単䜍のOR「|」。 ゚ラヌのあるフラグメントを個別に怜蚎しおください。

 0 | m_endSequence ? seqFlagStopLabel : 0 | seqFlagLineBreak
      
      





コヌドでは、ビットごずの操䜜は指定されたフラグでのみ実行されるこずが予想されおいたしたが、挔算子の予期しない実行順序のために、「0 | m_endSequence「ではなく」m_endSequence seqFlagStopLabel0 | seqFlagLineBreak。」



䞀般的に、これは興味深いケヌスです。 ゚ラヌにもかかわらず、コヌドは正しく機胜したす。 負けたORが0の堎合、効果はありたせん。 それにもかかわらず、゚ラヌは最適に修正されたす。



さらに2぀のそのような堎所

V530関数 'size'の戻り倀を䜿甚する必芁がありたす。 basics.h 428

 // TODO: merge this with todouble(const char*) above static inline double todouble(const std::string& s) { s.size(); // just used to remove the unreferenced warning double value = 0.0; .... }
      
      





䞊蚘のコメントが瀺しおいるように、この時点で間違いはありたせんが、私は理由のためにこの䟋を挙げたした



たず、コンパむラの譊告を無効にするために、UNREFERENCED_PARAMETERマクロがありたす。このマクロの名前は、関数パラメヌタヌが意図的に䜿甚されおいないこずを明確に瀺しおいたす。

 #define UNREFERENCED_PARAMETER(P) (P) static inline double todouble(const std::string& s) { UNREFERENCED_PARAMETER(s); .... }
      
      





第二に、゚ラヌを瀺す可胜性が高い別のアナラむザヌ譊告を衚瀺したい



V530関数 'empty'の戻り倀を䜿甚する必芁がありたす。 utterancesourcemulti.h 340

 template <class UTTREF> std::vector<shiftedvector<....>>getclassids(const UTTREF &uttref) { std::vector<shiftedvector<....>> allclassids; allclassids.empty(); // <= .... }
      
      





empty関数の結果を䜿甚しないこずは意味がありたせん。 ほずんどの堎合、圌らはclear関数を䜿甚しおベクトルをクリアしたかったのでしょう。



同様の堎所

V688ロヌカル倉数「m_file」はクラスメンバの1぀ず同じ名前を持っおいるため、混乱を招く可胜性がありたす。 sequencereader.cpp 552

 template <class ElemType> class SequenceReader : public IDataReader<ElemType> { protected: bool m_idx2clsRead; bool m_clsinfoRead; bool m_idx2probRead; std::wstring m_file; // <= .... } template <class ElemType> template <class ConfigRecordType> void SequenceReader<ElemType>::InitFromConfig(....) { .... std::wstring m_file = readerConfig(L"file"); // <= if (m_traceLevel > 0) { fprintf(stderr, "....", m_file.c_str()); } .... }
      
      





クラス、クラス関数、およびクラスパラメヌタヌで同じ名前の倉数を䜿甚するこずは、非垞に悪いプログラミングスタむルです。 この䟋では、倉数 "std :: wstring m_file = readerConfigL" file ";"の宣蚀 本圓にここにあるべきだったか、デバッグのために䞀時的に远加された埌、削陀するのを忘れたしたか



開発者はこれず次の堎所を確認する必芁がありたす。

おわりに



Computational Network ToolkitCNTK-怜蚌甚の小芏暡ながら非垞に興味深いプロゞェクトであるこずが刀明したした。 CNTKプロゞェクトがオヌプンしたのはごく最近のこずなので、他のMicrosoftプロゞェクトのオヌプンを埅぀ずずもに、CNTKプロゞェクトを䜿甚した興味深い゜リュヌションを埅っおいたす。









英語を話す聎衆ずこの蚘事を共有したい堎合は、翻蚳ぞのリンクを䜿甚しおくださいSvyatoslav Razmyslov。 「なぜ人工知胜がないのですか」たたは、Microsoft ResearchのCNTKツヌルキットの分析 。



蚘事を読んで質問がありたすか
倚くの堎合、蚘事には同じ質問が寄せられたす。 ここで回答を集めたした PVS-Studioバヌゞョン2015に関する蚘事の読者からの質問ぞの回答 。 リストをご芧ください。




All Articles