В 2018 году Microsoft создала ML.NET, платформу машинного обучения для разработчиков .NET. С тех пор библиотека машинного обучения претерпела значительные изменения и приобрела новые функции для выявления закономерностей в данных. Давайте посмотрим, как эти изменения повлияли на качество исходного кода ML.NET. И, конечно же, изучить ошибки!

Введение

Прежде чем мы начнем, давайте поближе познакомимся с фреймворком. Microsoft создала ML.NET как бесплатную библиотеку машинного обучения с открытым исходным кодом для языков программирования C# и F#. ML.NET позволяет вам создавать функциональные возможности для автоматических прогнозов, используя набор данных, доступный вашему приложению.

Вы можете использовать ML.NET для обучения пользовательской модели машинного обучения, указав алгоритм, или вы можете импортировать предварительно обученные модели TensorFlow и ONNX.

Вы можете добавить модель в свое приложение, чтобы получать прогнозы.

На момент написания ML.NET включала следующее: классификацию/категоризацию, регрессию, обнаружение аномалий, реализацию рекомендательных систем, классификацию изображений, временные ряды и последовательный анализ данных.

Зная, что такое ML.NET, теперь мы можем перейти к изучению ошибок, которые обнаружил PVS-Studio. Но в первую очередь следует отдать должное разработчикам Microsoft, так как код проекта качественный: найти какие-то ужасные или любопытные ошибки для этой статьи было довольно сложно. Тем не менее, есть несколько критических вопросов, заслуживающих внимания.

Мы проверили версию ML.NET 1.7.1. Исходный код версии этого проекта доступен на GitHub.

Итак, давайте погрузимся и посмотрим, что PVS-Studio нашел в ML.NET!

Обзор ошибок ML.NET

Объект использовался после нулевой проверки

Проблема 1

Предупреждение PVS-Studio: V3125. Объект оценщики использовался после того, как он был проверен на нуль. EstimatorChain.cs 37, 39

Рассмотрим предупреждение более подробно. PVS-Studio обнаружил возможную ошибку, которая могла привести к разыменованию нулевой ссылки. Давайте изучим код. Оператор объединения с нулевым значением проверяет, содержит ли входной параметр оценщики значение. Результат оператора записывается в поле _estimators.

В строке ниже разработчики снова используют параметр estimators, вызывая метод LastOrDefault. Доступ к параметру estimators, который может иметь нулевое значение, может вызвать NullReferenceException.

Решение этой проблемы заключается в использовании поля _estimators — вместо переменной estimators — для вызова метода LastOrDefault:

Имена параметра estimators и поля класса _estimators очень похожи. Разработчики, возможно, просто перепутали их. При написании кода они легко могли упустить такую ​​маленькую деталь. Но статический анализатор обращает внимание даже на мельчайшие элементы кода. Таким образом, вам не придется искать эти ошибки и исправлять их потом.

Два оператора if с одинаковыми условными выражениями

Проблема 2

Предупреждение PVS-Studio: V3021 Два оператора if с одинаковыми условными выражениями. Первый оператор if содержит возврат метода. Это означает, что второе утверждение если бессмысленно. BoostingFastTree.cs 44, 47

Анализатор обнаружил два похожих оператора if с одинаковыми условными выражениями. Программа завершается, так как блок if генерирует исключение. Это может показаться бесполезной кучей кода, но давайте посмотрим на проблему под другим углом. Посмотрите внимательнее на сообщения об ошибках исключений — они не идентичны. Вероятно, разработчики намеревались реализовать две разные проверки. Возможно, они скопировали чек, изменили сообщение, но забыли исправить условие. Таким образом, одна возможная ошибка осталась необработанной и до сих пор представляет угрозу для полноценной и корректной работы программы.

Переменная использовалась после того, как ей было присвоено значение с помощью нулевого условного оператора

Проблема 3

Предупреждение PVS-Studio: V3105 Переменная ‘mdType’ использовалась после того, как была присвоена через null-условный оператор. NullReferenceException возможно. PipelineEnsemble.cs 670

На этот раз анализатор выдает предупреждения о возможном возникновении NullReferenceException при разыменовании переменной, которая может иметь значение null. Анализатор так считает, потому что между присваиванием значения и его использованием нет проверки на null. Посмотрите на код. Переменной mdType присваивается значение через цепочку вызовов. Одной из частей цепочки является метод GetColumnOrNull, возвращающий null при определенном условии:

Сразу после присвоения значения осуществляется доступ к переменной mdType для вызова метода Equals в операторе условия. Код такого типа может привести к исключению. Разработчики видимо забыли обработать этот случай проверкой.

Неправильный порядок аргументов, переданных в метод

Проблема 4

Предупреждение PVS-Studio: V3066. Возможен неправильный порядок аргументов, передаваемых в метод. ProgressReporter.cs 148

Анализатор заметил, что порядок аргументов, передаваемых в метод, возможно, неверен. Действительно, метод StartProgressChannel возвращает экземпляр класса SubChannel, конструктор которого получил параметры в неправильном порядке (id и level перепутаны). Это видно из реализации класса SubChannel:

Похоже на простую опечатку! Тем не менее, это может вызвать серьезные проблемы. Обратите внимание на реализацию класса конструктора SubChannel. Здесь полям _id и _level присвоены неверные данные. Эти поля могут негативно повлиять на работу класса.

На самом деле такие ошибки часто случаются в масштабных проектах. Как видите, ML.NET не исключение. Вот еще один пример:

Проблема 5

Предупреждение PVS-Studio: V3066. Возможен неверный порядок аргументов, передаваемых в метод AllocateModelMemory: numVocab и numTopic. LdaSingleBox.cs 142

Здесь при вызове метода LdaInterface.AllocateModelMemory входные параметры (numTopic и numVocab) перепутаны. Это видно из того, как параметры были переданы в сигнатуру метода:

Объект использовался после проверки на null

Проблема 6

Предупреждение PVS-Studio: V3125. Калиброванный объект использовался после того, как он был проверен на нуль. TreeEnsembleCombiner.cs 54, 58, 59

Вот еще один способ увидеть исключение NullReferenceException. Обратите внимание на переменную с калибровкой. Эта переменная инициализируется с помощью оператора as, который возвращает null, если преобразование типа не удается. Затем эта переменная используется без проверки:

Переменная калибровка проверяется на null три раза в разных частях метода. Можно смело сказать, что эта проблема весьма актуальна для проекта ML.NET.

Отмечу, что именно в этом проекте чаще всего встречаются такие ошибки, поэтому предлагаю рассмотреть еще несколько любопытных случаев:

Проблема 7

Предупреждение PVS-Studio: V3125. Объект _instanceWeights использовался после того, как он был проверен на значение null. InternalQuantileRegressionTree.cs 81, 74

Здесь проверка null поля _instanceWeights реализована с помощью метода Contracts.Check:

Если _instanceWeights имеет значение null, метод Contracts.Check обработает ошибку, выдав NullReferenceException. Вроде бы это была изначальная идея разработчика, но все вышло иначе. Обратите внимание, как реализован метод Contracts.Check. Если _instanceWeights == null имеет значение true, значение true передается в метод Contracts.Check как f параметр, а затем значение преобразуется в false. В результате проверка на null была бы неправильной, и код продолжал бы выполняться. При следующем доступе к _instanceWeights будет выдано NullReferenceException.

Проблема 8

Предупреждение PVS-Studio: V3125. Объект srcColType использовался после того, как он был проверен на значение null. Проверить строки: 843, 842. LdaTransform.cs 842, 843

Этот случай показался мне любопытным, так как здесь вроде бы все было в порядке:

  • преобразование типа выполняется через оператор as, после чего результат записывается в переменную srcColType;
  • переменная srcColType проверяется на null, и если проверка проходит успешно, выдается исключение с сообщением об ошибке.

И все было бы хорошо. Однако результат метода srcColType.ToString(), вызванного для потенциально пустой переменной, передается в качестве последнего параметра исключения:

....
throw env.ExceptSchemaMismatch(...., srcColType.ToString());
....

Хотя эта ошибка не кажется очень серьезной, она все же имеет довольно неприятное последствие: теряется некоторая необходимая для отладки информация (сообщение об ошибке). В этом случае информация особенно важна, так как ошибка находится в цикле, а количество итераций в цикле может достигать нескольких десятков. В подобных случаях нам нужно выяснить, в каком контексте произошла ошибка.

Выражение всегда ложно

Проблема 9

Предупреждения PVS-Studio:

  • В3022. Выражение ‘type == IntArrayType.Sparse’ всегда ложно. IntArray.cs 145
  • В3063. Часть условного выражения всегда ложна, если она оценивается: type == IntArrayType.Dense. IntArray.cs 129

В этом случае два предупреждения были вызваны одной и той же причиной. Это еще один любопытный случай, так что давайте углубимся в него. При заданных условиях type может иметь значение IntArrayType.Sparse или IntArrayType.Dense. Но анализатор считает это неправильным. Анализатор прав? Давайте разберем это дело. Посмотрите на следующий код:

Contracts.CheckParam(type == IntArrayType.Current || 
                     type == IntArrayType.Repeat || 
                     type == IntArrayType.Segmented, nameof(type));

Здесь метод Contracts.CheckParam используется для проверки того, имеет ли входной параметр type одно из следующих значений: IntArrayType.Current, IntArrayType.Repeat или IntArrayType.Segmented.

Теперь обратим внимание на реализацию метода Contracts.CheckParam:

public static void CheckParam(bool f, string paramName)
{
  if (!f)
    throw ExceptParam(paramName);
}

Как видите, если type не равен ни одному из указанных выше значений, генерируется исключение.

В результате у нас есть два набора значений для входного параметра type:

  • ожидаемые значения (IntArrayType.Sparse,IntArrayType.Dense);
  • допустимые значения (IntArrayType.Current, IntArrayType.Repeat, IntArrayType.Segmented).

Очевидно, значения совершенно другие, так что здесь могла быть допущена серьезная ошибка. Дело раскрыто!

Можно сказать, что разработчики не планировали перебирать все допустимые значения при реализации вышеуказанной проверки. Но они намеревались отфильтровать недопустимые значения для этого метода. Поэтому правильная реализация этой проверки может быть следующей:

Contracts.CheckParam(type != IntArrayType.Current || 
                     type != IntArrayType.Repeat || 
                     type != IntArrayType.Segmented, nameof(type));

Заключение

Анализатор обнаружил лишь несколько серьезных ошибок в проекте ML.NET (в большинстве случаев «человеческие» ошибки). Так что да, команда Microsoft знает свою работу. Но, в любом случае, даже профессионалы могут ошибаться. Так почему бы не использовать статический анализатор в своем проекте, тем самым минимизировать количество ошибок и сделать ваши проекты еще лучше и надежнее?

Вы можете использовать PVS-Studio бесплатно, скачав пробную версию здесь.

Рекомендуем заранее изучить основы использования PVS-Studio в документации.

В конце концов, хочется пожелать компании успехов в разработке фреймворка машинного обучения ML.NET. Мне очень интересен этот проект!

Итак, я надеюсь, что вы нашли эту статью любопытной :).

Удачи и чистого кода вам! Спасибо и до скорой встречи!