Comment un code étrange cache-t-il les erreurs? Analyse TensorFlow.NET

TensorFlow.NET et PVS-Studio

L'analyse statique est un outil extrêmement utile pour tout développeur, car elle aide à trouver dans le temps non seulement des erreurs, mais aussi des morceaux de code suspects et étranges qui peuvent dérouter les programmeurs qui devraient travailler avec lui à l'avenir. Cette idée sera démontrée par une analyse du projet TensorFlow.NET open C #, qui est en cours de développement pour fonctionner avec la populaire bibliothèque d'apprentissage automatique TensorFlow.

Je m'appelle Nikita Lipilin. Il y a quelque temps, j'ai rejoint le département des programmeurs C # de PVS-Studio. Traditionnellement, tous les nouveaux venus dans l'équipe écrivent des articles qui discutent des résultats de la vérification de divers projets ouverts à l'aide de l'analyseur statique PVS-Studio. Ces articles aident les nouveaux employés à mieux connaître le produit et offrent en même temps des avantages supplémentaires en termes de vulgarisation de la méthodologie d'analyse statique. Je vous propose de vous familiariser avec mes premiers travaux sur le thème de l'analyse des projets ouverts.

introduction


La variété des erreurs possibles dans le code du programme est incroyable. Certains d'entre eux se révèlent immédiatement, lors d'un examen de surface de l'application créée, d'autres peuvent être difficiles à remarquer même lors d'une révision de code par une équipe de développeurs expérimentés. Cependant, il arrive également que, en raison d'une inattention ou d'une autre raison, le programmeur écrive parfois du code simplement étrange et illogique, qui, néanmoins, semble (semble) remplir avec succès sa fonction. Mais seulement plus loin, en revenant à ce qui a été écrit, ou lorsque d'autres étudient ce code, des questions commencent à se poser auxquelles il est impossible de répondre.

La refactorisation de l'ancien code peut se transformer en problèmes, surtout si d'autres parties du programme en dépendent, donc quand vous trouvez même des constructions franchement incurvées, le ″ ça marche? Ne le touchez pas! ″. Par la suite, cela rend difficile l'étude de la source et, par conséquent, il devient difficile d'étendre les capacités disponibles. La base de code est obstruée, il est probable que certains problèmes internes petits et invisibles, mais potentiellement très désagréables, ne soient pas résolus à temps.

À un moment donné, les conséquences de cette erreur se feront sentir, mais sa recherche prendra beaucoup de temps, car les soupçons du développeur tomberont sur un grand nombre de fragments de code étranges qui n'ont pas été refactorisés en même temps. Il en résulte que divers problèmes et bizarreries dans un fragment particulier doivent être corrigés immédiatement après son écriture. Dans le cas où il y a des raisons raisonnables de tout laisser tel quel (par exemple, si le code est écrit comme une sorte de blanc "pour plus tard"), un tel fragment doit être accompagné d'un commentaire explicatif.

Il convient également de noter que, quelles que soient les qualifications du développeur, certains moments problématiques et simplement infructueux peuvent lui échapper, et parfois une "solution temporaire" peut être appliquée à un endroit, qui sera bientôt oubliée et deviendra "permanente". Par la suite, l'analyse d'un tel code (très probablement, un autre développeur sera impliqué dans cela) demandera beaucoup d'efforts.

Dans de tels cas, la révision du code peut aider. Cependant, si la tâche est assez sérieuse, cette option nécessitera beaucoup de temps. De plus, lorsqu'il y a beaucoup de petites erreurs ou lacunes, l'inspecteur risque de ne pas remarquer d'erreurs de haut niveau derrière elles. La vérification du code devient une routine fastidieuse, et progressivement l'efficacité de l'examen diminue.

De toute évidence, les tâches de routine seront beaucoup mieux transférées de la personne à l'ordinateur. Cette approche est utilisée dans de nombreux domaines de la modernité. L'automatisation de divers processus est la clé de la prospérité. Qu'est-ce que l'automatisation dans le contexte de ce sujet?

Un assistant fiable pour résoudre le problème de l'écriture d'un code de travail raisonnable et stable est l'analyse statique. Chaque fois avant d'envoyer les résultats de leurs activités à la revue, le programmeur pourra effectuer une vérification automatisée et ne pas surcharger les autres développeurs (et lui-même) de travail inutile. Le code ne sera envoyé pour vérification qu'après avoir pris en compte tous les avertissements de l'analyseur: des erreurs ont été corrigées et des moments étranges ont été réécrits ou du moins expliqués par un commentaire.

Bien sûr, la nécessité de revoir le code ne disparaît pas, mais l'analyse statique complète et simplifie considérablement sa mise en œuvre. Une partie suffisamment importante des erreurs sera corrigée grâce à l'analyseur, et les moments étranges ne seront certainement pas oubliés et marqués en conséquence. En conséquence, avec une révision du code, il sera possible de se concentrer sur la vérification de l'exactitude de la mise en œuvre d'interactions logiques complexes et de trouver des problèmes sous-jacents qui, malheureusement, ne peuvent pas être détectés par l'analyseur (jusqu'à présent).

TensorFlow.NET




Cet article est inspiré du projet TensorFlow.NET. Il représente la mise en œuvre de la capacité de travailler avec les fonctionnalités de la bibliothèque d'apprentissage machine TensorFlow populaire via le code C # (d'ailleurs, nous l'avons également testé ). Cette idée semblait assez intéressante, car au moment de la rédaction, travailler avec la bibliothèque n'est disponible qu'en Python, Java et Go.

Le code source disponible sur GitHub est constamment mis à jour et maintenant son volume est un peu plus de cent mille lignes. Après une étude superficielle, il y avait un désir de procéder à une vérification en utilisant une analyse statique. PVS-Studio a été utilisé comme un outil spécifique, qui a prouvé son efficacité dans un assez grand nombre de projets différents .


Pour TensorFlow.NET, l'analyseur a affiché un certain nombre d'alertes: 39 niveaux élevés, 227 niveaux moyens et 154 niveaux bas (vous pouvez lire les niveaux d'avertissement ici dans la sous-section «Niveaux d'avertissement et ensembles de règles de diagnostic»). Une analyse détaillée de chacun d'eux rendrait cet article gigantesque, donc seuls ceux qui me paraissent les plus intéressants sont décrits ci-dessous. Il convient également de noter que certains des problèmes rencontrés sont rencontrés plusieurs fois dans le projet, et l'analyse de chacun de ces morceaux de code dépasse le cadre de cet article.

Le projet se donne une tâche assez sérieuse et, malheureusement, l'apparition de divers types de fragments de code étranges est inévitable. Dans cet article, je vais essayer de montrer que l'utilisation de l'analyse statique peut grandement simplifier le travail des programmeurs, en pointant les zones susceptibles de poser des questions. Pas toujours un avertissement indique une erreur, mais assez souvent il indique un code qui pourrait poser des questions à une personne. En conséquence, la probabilité qu'un code étrange soit remanié ou, en tout cas, commenté en conséquence est considérablement augmentée.

Fragments qui ont attiré l'attention lors de l'étude du rapport de l'analyseur


En fait, un assez grand nombre d'avertissements d'analyseur pour ce projet peuvent être appelés non pas tant d'erreurs, mais plutôt de code étrange. Lors de l'affichage des lignes pour lesquelles un avertissement est émis, au moins la perplexité survient. Bien sûr, certains des exemples ci-dessous sont probablement des "solutions temporaires", mais ils ne donnent aucun commentaire sur ce problème, ce qui signifie que quelqu'un qui travaillera avec ce code à l'avenir aura des questions, recherchant des réponses auxquelles prendra du temps supplémentaire.


Dans le même temps, certains avertissements indiquent un code qui n'est évidemment pas seulement étrange, mais tout simplement faux. C'est le principal danger d'un "code étrange" - il est extrêmement difficile de remarquer une véritable erreur si vous voyez ici et là des solutions incompréhensibles et que vous vous habituez progressivement au fait que le code semble incorrect.

Visite de collection sophistiquée


private static void _RemoveDefaultAttrs(....)
{
  var producer_op_dict = new Dictionary<string, OpDef>();
  producer_op_list.Op.Select(op =>
  {
    producer_op_dict[op.Name] = op;
    return op;
  }).ToArray();           
  ....
}

Avertissement de l'analyseur : V3010 La valeur de retour de la fonction 'ToArray' doit être utilisée. importer.cs 218

L'analyseur considère à ce stade l'appel à Toray comme suspect, car la valeur renvoyée par cette fonction n'est pas affectée à une variable. Cependant, un tel code n'est pas une erreur. Cette construction est utilisée pour remplir le dictionnaire producteur_op_dict avec des valeurs correspondant aux éléments de la liste producteur_op_list.Op . Un appel à ToArray est nécessaire pour que la fonction passée en argument à la méthode Select soit appelée pour tous les éléments de la collection.

À mon avis, le code ne semble pas le meilleur. Remplir le dictionnaire n'est pas évident et certains développeurs peuvent vouloir supprimer l'appel «inutile» de ToArray . Il serait beaucoup plus simple et plus compréhensible d'utiliser la boucle foreach ici :

var producer_op_dict = new Dictionary<string, OpDef>();

foreach (var op in producer_op_list.Op)
{
  producer_op_dict[op.Name] = op;
}

Dans ce cas, le code semble aussi simple que possible.

Un autre moment similaire ressemble à ceci:

public GraphDef convert_variables_to_constants(....)
{
  ....
  inference_graph.Node.Select(x => map_name_to_node[x.Name] = x).ToArray();
  ....
}

Avertissement de l'analyseur : V3010 La valeur de retour de la fonction 'ToArray' doit être utilisée. graph_util_impl.cs 48

La seule différence est qu'un tel enregistrement semble plus concis, mais seule la tentation de supprimer l'appel ToArray ici ne disparaît pas, et il ne semble toujours pas évident.

Solution temporaire


public GraphDef convert_variables_to_constants(....)
{
  ....
  var source_op_name = get_input_name(node);
  while(map_name_to_node[source_op_name].Op == "Identity")
  {
    throw new NotImplementedException);
    ....
  }
  ....
}

Avertissement de l'analyseur : V3020 Un «lancer» inconditionnel dans une boucle. graph_util_impl.cs 73

Dans le projet considéré, l'approche suivante est souvent utilisée: si un comportement doit être implémenté ultérieurement, une exception NotImplementedException est levée à l'endroit approprié . Il est clair pourquoi l'analyseur met en garde contre une erreur possible dans cette pièce: utiliser while au lieu de if ne semble pas vraiment trop raisonnable.

Ce n'est pas le seul avertissement qui apparaît en raison de l'utilisation de solutions temporaires. Par exemple, il existe une méthode:

public static Tensor[] _SoftmaxCrossEntropyWithLogitsGrad(
  Operation op, Tensor[] grads
)
{
  var grad_loss = grads[0];
  var grad_grad = grads[1];
  var softmax_grad = op.outputs[1];
  var grad = _BroadcastMul(grad_loss, softmax_grad);

  var logits = op.inputs[0];
  if(grad_grad != null && !IsZero(grad_grad)) // <=
  {
    throw new NotImplementedException("_SoftmaxCrossEntropyWithLogitsGrad");
  }

  return new Tensor[] 
  {
    grad,
    _BroadcastMul(grad_loss, -nn_ops.log_softmax(logits))
  };
}

Avertissement de l'analyseur : l' expression V3022 «grad_grad! = Null &&! IsZero (grad_grad)» est toujours fausse. nn_grad.cs 93

En fait, une exception NotImplementedException ("_ SoftmaxCrossEntropyWithLogitsGrad") ne sera jamais levée, car ce code est tout simplement inaccessible. Pour démêler la raison, vous devez vous rendre dans le code de la fonction IsZero :

private static bool IsZero(Tensor g)
{
  if (new string[] { "ZerosLike", "Zeros" }.Contains(g.op.type))
    return true;

  throw new NotImplementedException("IsZero");
}

La méthode retourne true ou lève une exception. Ce code n'est pas une erreur - évidemment, l'implémentation ici est laissée pour plus tard. L'essentiel est que "alors" soit vraiment arrivé. Eh bien, il s'est avéré très bien que PVS-Studio ne vous laissera pas oublier qu'il y a une telle imperfection ici :)

Est-ce que Tenseur est Tenseur?


private static Tensor[] _ExtractInputShapes(Tensor[] inputs)
{
  var sizes = new Tensor[inputs.Length];
  bool fully_known = true;
  for(int i = 0; i < inputs.Length; i++)
  {
    var x = inputs[i];

    var input_shape = array_ops.shape(x);
    if (!(input_shape is Tensor) || input_shape.op.type != "Const")
    {
      fully_known = false;
      break;
    }

    sizes[i] = input_shape;
  }
  ....
}

Avertissement de l'analyseur : V3051 Une vérification de type excessive. L'objet est déjà de type 'Tenseur'. array_grad.cs 154

Le type de retour de la méthode de forme est Tensor . Donc, la valeur input_shape est Tensor check semble au moins bizarre. Peut-être, une fois que la méthode a renvoyé une valeur d'un type différent et que la vérification a eu un sens, mais il est également possible qu'au lieu de Tensor, la condition spécifie une sorte d'héritier de cette classe. D'une manière ou d'une autre, le développeur doit prêter attention à ce fragment.

Vérification approfondie


public static Tensor[] _BaseFusedBatchNormGrad(....)
{
  ....
  if (data_format == "NCHW") // <=
    throw new NotImplementedException("");

  var results = grad_fun(new FusedBatchNormParams
  {
    YBackprop = grad_y,
    X = x,
    Scale = scale,
    ReserveSpace1 = pop_mean,
    ReserveSpace2 = pop_var,
    ReserveSpace3 = version == 2 ? op.outputs[5] : null,
    Epsilon = epsilon,
    DataFormat = data_format,
    IsTraining = is_training
  });

  var (dx, dscale, doffset) = (results[0], results[1], results[2]);
  if (data_format == "NCHW") // <=
    throw new NotImplementedException("");

  ....
}

Avertissements de l'analyseur :

  • V3021 Il existe deux instructions 'if' avec des expressions conditionnelles identiques. La première instruction «if» contient la méthode return. Cela signifie que la deuxième instruction «if» est insensée nn_grad.cs 230
  • V3022 L' expression 'data_format == "NCHW"' est toujours fausse. nn_grad.cs 247

Contrairement à certains des exemples précédents, il y a clairement un problème avec le code. La deuxième vérification n'a aucun sens, car si la condition est vraie, l'exécution du programme ne l'atteindra pas du tout. Peut-être que certaines fautes de frappe sont autorisées ici, ou l'une des vérifications est généralement superflue.

L'illusion du choix


public Tensor Activate(Tensor x, string name = null)
{
  ....
  Tensor negative_part;
  if (Math.Abs(_threshold) > 0.000001f)
  {
    negative_part = gen_ops.relu(-x + _threshold);
  } else
  {
    negative_part = gen_ops.relu(-x + _threshold);
  }
  ....
}

Avertissement de l'analyseur : V3004 L' instruction 'then' est équivalente à l'instruction 'else'. gen_nn_ops.activations.cs 156

Une démonstration plutôt amusante de l'efficacité de l'utilisation de l'analyse statique dans le développement. Il est difficile de trouver une raison raisonnable pour laquelle le développeur a écrit ce code particulier - très probablement, il s'agit d'une erreur de copier-coller typique (bien que cela, bien sûr, puisse être un autre "pour plus tard").

Il existe d'autres fragments d'un plan similaire, par exemple:

private static Operation _GroupControlDeps(
  string dev, Operation[] deps, string name = null
)
{
  return tf_with(ops.control_dependencies(deps), ctl =>
  {
    if (dev == null)
    {
      return gen_control_flow_ops.no_op(name);
    }
    else
    {
      return gen_control_flow_ops.no_op(name);
    }
  });
}

Avertissement de l'analyseur : V3004 L' instruction 'then' est équivalente à l'instruction 'else'. control_flow_ops.cs 135

Peut-être une fois que la vérification avait du sens, mais au fil du temps elle a disparu, ou d'autres changements supplémentaires sont prévus à l'avenir. Cependant, ni l'une ni l'autre option ne semble être une justification suffisante pour laisser quelque chose de similaire dans le code, sans expliquer cette bizarrerie en aucune façon. Avec une probabilité élevée, une erreur de copier-coller a été faite exactement de la même manière.

Chèque tardif


public static Tensor[] Input(int[] batch_shape = null,
  TF_DataType dtype = TF_DataType.DtInvalid,
  string name = null,
  bool sparse = false,
  Tensor tensor = null)
{
  var batch_size = batch_shape[0];
  var shape = batch_shape.Skip(1).ToArray(); // <=

  InputLayer input_layer = null;
  if (batch_shape != null)                   // <=
    ....
  else
    ....

  ....
}

Avertissement de l'analyseur : V3095 L'objet 'batch_shape' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 39, 42. keras.layers.cs 39

Une erreur classique et plutôt dangereuse de l'utilisation potentielle d'une variable, qui n'est un lien vers nulle part. Dans ce cas, le code implique explicitement la possibilité que batch_shape soit null - cela ressort clairement à la fois de la liste des arguments et de la vérification ultérieure de la même variable. Ainsi, l'analyseur indique ici une erreur claire.

Un autre "pour plus tard"?


public MnistDataSet(
  NDArray images, NDArray labels, Type dataType, bool reshape // <=
) 
{
  EpochsCompleted = 0;
  IndexInEpoch = 0;

  NumOfExamples = images.shape[0];

  images = images.reshape(
    images.shape[0], images.shape[1] * images.shape[2]
  );
  images = images.astype(dataType);
  // for debug np.multiply performance
  var sw = new Stopwatch();
  sw.Start();
  images = np.multiply(images, 1.0f / 255.0f);
  sw.Stop();
  Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
  Data = images;

  labels = labels.astype(dataType);
  Labels = labels;
}

Avertissement de l'analyseur : le paramètre de constructeur V3117 'remodeler' n'est pas utilisé. MnistDataSet.cs 15

Comme certaines autres bizarreries, cela est probablement dû au fait que la fonctionnalité est loin d'être complètement implémentée et il est fort possible que le paramètre de remodelage soit utilisé d'une manière ou d'une autre dans ce constructeur à l'avenir. Mais pour le moment, il semble qu'il soit jeté ici comme ça. Si cela est vraiment fait "pour plus tard", alors il serait sage de le marquer avec un commentaire. Sinon, il s'avère que le code qui construit l'objet devra lancer un paramètre supplémentaire dans le constructeur, et il est fort possible qu'il serait plus pratique de ne pas le faire.

Déréférence nulle possible insaisissable


public static Tensor[] _GatherV2Grad(Operation op, Tensor[] grads)
{
  ....
  if((int)axis_static == 0)
  {
    var params_tail_shape = params_shape.slice(new NumSharp.Slice(start:1));
    var values_shape = array_ops.concat(
      new[] { indices_size, params_tail_shape }, 0
    );
    var values = array_ops.reshape(grad, values_shape);
    indices = array_ops.reshape(indices, indices_size);
    return new Tensor[]
    {
      new IndexedSlices(values, indices, params_shape), // <=
      null,
      null
    };
  }
  ....
}

Avertissement de l'analyseur : V3146 Déréférence nulle possible du 1er argument 'valeurs' à l'intérieur de la méthode. Le '_outputs.FirstOrDefault ()' peut retourner la valeur nulle par défaut. array_grad.cs 199

Pour comprendre le problème, vous devez d'abord vous tourner vers le code constructeur indexedSlices :

public IndexedSlices(
  Tensor values, Tensor indices, Tensor dense_shape = null
)
{
  _values = values;
  _indices = indices;
  _dense_shape = dense_shape;

  _values.Tag = this; // <=
}

Évidemment, passer null à ce constructeur lèvera une exception. Cependant, pourquoi l'analyseur considère-t-il que la variable values peut contenir null ?

PVS-Studio utilise la technique d'analyse de flux de données, qui vous permet de trouver les ensembles de valeurs possibles de variables dans différentes parties du code. Un avertissement vous indique que null dans la variable spécifiée peut être renvoyé avec la ligne suivante: _outputs.FirstOrDefault () . Cependant, en regardant le code ci-dessus, vous pouvez constater que la valeur de la variable values est obtenue en appelant array_ops.reshape (grad, values_shape) . Alors d'où vient _outputs.FirstOrDefault () ?

Le fait est que lors de l'analyse du flux de données, non seulement la fonction actuelle est prise en compte, mais aussi toutes appelées; PVS-Studio obtient des informations sur l'ensemble des valeurs possibles de n'importe quelle variable n'importe où. Ainsi, un avertissement signifie que l'implémentation de array_ops.reshape (grad, values_shape) contient un appel à _outputs.FirstOrDefault () , dont le résultat est finalement renvoyé.

Pour vérifier cela, accédez à l'implémentation de remodelage :

public static Tensor reshape<T1, T2>(T1 tensor, T2 shape, string name = null)
            => gen_array_ops.reshape(tensor, shape, null);

Ensuite, allez à la méthode de remodelage appelée à l'intérieur:
public static Tensor reshape<T1, T2>(T1 tensor, T2 shape, string name = null)
{
  var _op = _op_def_lib._apply_op_helper(
    "Reshape", name, new { tensor, shape }
  );
  return _op.output;
}

La fonction _apply_op_helper renvoie un objet de la classe Operation qui contient la propriété de sortie . C'est à réception de sa valeur que le code décrit dans l'avertissement est appelé:

public Tensor output => _outputs.FirstOrDefault();

Le tenseur est, bien sûr, un type de référence, donc la valeur par défaut pour lui sera nulle . De tout cela, on peut voir que PVS-Studio analyse méticuleusement la structure logique du code, pénétrant profondément dans la structure des appels.

L'analyseur a terminé son travail, indiquant un endroit potentiellement problématique. Le programmeur doit vérifier si une situation peut survenir lorsque des éléments dans _outputs sont absents.

Ainsi, l'analyse statique obligera au moins le développeur à prêter attention au fragment suspect afin d'évaluer si l'erreur peut réellement s'y produire. Avec cette approche, le nombre d'erreurs qui passent inaperçu sera rapidement réduit.

Attente peu fiable?


private (LoopVar<TItem>, Tensor[]) _BuildLoop<TItem>(
  ....
) where ....
{
  ....
  // Finds the closest enclosing non-None control pivot.
  var outer_context = _outer_context;
  object control_pivot = null;
  while (outer_context != null && control_pivot == null) // <=
  {

  }

  if (control_pivot != null)
  {

  }
  ....
}

Avertissement de l'analyseur : V3032 L' attente de cette expression n'est pas fiable, car le compilateur peut optimiser certaines des variables. Utilisez des variables volatiles ou des primitives de synchronisation pour éviter cela. WhileContext.cs 212

L'analyseur indique qu'une telle implémentation de l'attente peut être optimisée par le compilateur, mais je doute qu'ils aient vraiment essayé d'implémenter l'attente ici - très probablement, le code n'a tout simplement pas été ajouté et il sera développé à l'avenir. Il peut être utile de lancer une exception NotImplementedException , étant donné que cette pratique est utilisée ailleurs dans le projet. D'une manière ou d'une autre, je pense que certains commentaires explicatifs ne feraient évidemment pas de mal.

Briser les frontières


public TensorShape(int[][] dims)
{
  if(dims.Length == 1)
  {
    switch (dims[0].Length)
    {
      case 0: shape = new Shape(new int[0]); break;
      case 1: shape = Shape.Vector((int)dims[0][0]); break;
      case 2: shape = Shape.Matrix(dims[0][0], dims[1][2]); break; // <=
      default: shape = new Shape(dims[0]); break;
    }
  }
  else
  {
    throw new NotImplementedException("TensorShape int[][] dims");
  }
}

Avertissement de l'analyseur : V3106 L' index est peut-être hors limite. L'index «1» pointe au-delà de la limite «dims». TensorShape.cs 107

Parmi les extraits de code étranges que j'ai vus, il y avait une vraie erreur, qui est très difficile à remarquer. Le fragment suivant est erroné ici: dims [1] [2] . Obtenir un élément avec l'index 1 à partir d'un tableau d'un élément est évidemment une erreur. Dans le même temps, si vous changez le fragment en dims [0] [2] , une autre erreur apparaît - obtenir l'élément avec l'index 2 du tableau dims [0] , dont la longueur dans ce cas est la branche 2. Ainsi, ce problème s'est avéré être comme avec un "double fond".

Dans tous les cas, ce fragment de code doit être étudié et corrigé par le développeur. À mon avis, cet exemple est une excellente illustration des performances de l'analyse de flux de données dans PVS-Studio.

Olepatka?


private void _init_from_args(object initial_value = null, ....) // <=
{
  var init_from_fn = initial_value.GetType().Name == "Func`1"; // <=
  ....
  tf_with(...., scope =>
  {
    ....
    tf_with(...., delegate
    {
      initial_value = ops.convert_to_tensor(  // <=
        init_from_fn ? (initial_value as Func<Tensor>)():initial_value,
        name: "initial_value",
        dtype: dtype
      );
    });
    _shape = shape ?? (initial_value as Tensor).TensorShape;
    _initial_value = initial_value as Tensor; // <=
    ....
    _dtype = _initial_value.dtype.as_base_dtype(); // <=

    if (_in_graph_mode)
    {
      ....

      if (initial_value != null) // <=
      {
        ....
      }

      ....
    }

    ....
  });
}

Pour comprendre le code ci-dessus, il vaut également la peine d'introduire l'implémentation de la fonction tf_with:

[DebuggerStepThrough] // with "Just My Code" enabled this lets the 
[DebuggerNonUserCode()]  //debugger break at the origin of the exception
public static void tf_with<T>(
  T py, Action<T> action
) where T : ITensorFlowObject
{
  try
  {
    py.__enter__();
    action(py);
  }
  finally
  {
    py.__exit__();
    py.Dispose();
  }
}

Avertissement de l'analyseur : V3019 Il est possible qu'une variable incorrecte soit comparée à null après la conversion de type à l'aide du mot clé 'as'. Vérifiez les variables 'initial_value', '_initial_value'. ResourceVariable.cs 137

_init_from_args est une fonction assez volumineuse, donc de nombreux fragments ont été omis. Vous pouvez le voir complètement en cliquant sur le lien . Bien que l'avertissement au début ne m'ait pas semblé vraiment sérieux, après avoir étudié, j'ai réalisé que quelque chose n'allait vraiment pas avec le code.

Tout d'abord, il convient de noter que la méthode peut être appelée sans passer de paramètres et par défaut, elle sera nulle dans initial_value . Dans ce cas, une exception sera levée sur la première ligne. Deuxièmement, la vérification

valeur_initiale à l'inégalité nulle semble étrange: si la valeur initiale est vraiment devenue nulle après avoir appelé ops.convert_to_tensor , alors _initial_value serait nulle , ce qui signifie que l'appel de _initial_value.dtype.as_base_dtype () déclencherait également une exception.

Analyzer indique que vous devrez peut-être vérifier que null est _initial_value , mais comme indiqué ci-dessus, la référence à cette variable a lieu avant ce test, donc cette option serait également incorrecte.

Cette petite erreur serait-elle remarquée dans une fonction aussi géante sans PVS-Studio? J'en doute beaucoup.

Conclusion


Dans un projet avec de nombreux exemples de code étrange, de nombreux problèmes peuvent être cachés. Le programmeur, habitué à voir l'incompréhensible, cesse de remarquer des erreurs en même temps. Les conséquences peuvent être très tristes. En effet, parmi les avertissements de l'analyseur, il y en a aussi de faux, cependant, dans la plupart des cas, les avertissements indiquent au moins des fragments de code qui peuvent poser des questions lorsqu'ils sont vus par une personne. Dans le cas où le code étrange est écrit intentionnellement, cela vaut la peine de laisser des explications afin que le fragment soit clair pour le développeur qui travaillera avec ce code à l'avenir (même si cela signifie laisser des commentaires pour lui-même).

En même temps, les outils d'analyse statique, tels que PVS-Studio, peuvent être d'une grande aide pour trouver les erreurs et les bizarreries potentielles afin qu'elles soient visibles et non oubliées, et toutes les solutions temporaires sont ensuite affinées et transformées en un travail propre, structuré et stable le code.


Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Nikita Lipilin. Comment un code étrange masque-t-il les erreurs? Analyse TensorFlow.NET .

All Articles