[PYTHON] [PyTorch] Warum Sie eine Instanz von CrossEntropyLoss () wie eine Funktion behandeln können

Instanz = Funktion? ?? ?? ??

[Lerne beim Machen! Development Deep Learning von PyTorch](https://www.amazon.co.jp/%E3%81%A4%E3%81%8F%E3%82%8A%E3%81%AA%E3%81%8C% E3% 82% 89% E5% AD% A6% E3% 81% B6% EF% BC% 81PyTorch% E3% 81% AB% E3% 82% 88% E3% 82% 8B% E7% 99% BA% E5% B1% 95% E3% 83% 87% E3% 82% A3% E3% 83% BC% E3% 83% 97% E3% 83% A9% E3% 83% BC% E3% 83% 8B% E3% 83% B3% E3% 82% B0-% E5% B0% 8F% E5% B7% 9D-% E9% 9B% 84% E5% A4% AA% E9% 83% 8E-ebook / dp / B07VPDVNKW) Es gab eine solche Beschreibung in 1-3 Transferlernen. (Sie können den gesamten Code unter [Author GitHub] sehen (https://github.com/YutaroOgawa/pytorch_advanced/blob/master/1_image_classification/1-3_transfer_learning.ipynb))

1-3_transfer_learning.ipynb


#Paketimport
import glob
import os.path as osp
import random
import numpy as np
import json
from PIL import Image
from tqdm import tqdm
import matplotlib.pyplot as plt
%matplotlib inline

import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import torchvision
from torchvision import models, transforms

(Unterlassung)

#Einstellungen der Verlustfunktion
criterion = nn.CrossEntropyLoss()

(Unterlassung)

def train_model(net, dataloaders_dict, criterion, optimizer, num_epochs):

    #Epochenschleife
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch+1, num_epochs))
        print('-------------')

        #Lern- und Verifizierungsschleife für jede Epoche
        for phase in ['train', 'val']:
            if phase == 'train':
                net.train()  #Versetzen Sie das Modell in den Trainingsmodus
            else:
                net.eval()   #Versetzen Sie das Modell in den Validierungsmodus

            epoch_loss = 0.0  #Epochenverlustsumme
            epoch_corrects = 0  #Anzahl der richtigen Antworten für die Epoche

            #Epoche, um die Überprüfungsleistung zu überprüfen, wenn Sie nicht gelernt haben=0 Training weggelassen
            if (epoch == 0) and (phase == 'train'):
                continue

            #Schleife zum Abrufen des Mini-Batch vom Datenlader
            for inputs, labels in tqdm(dataloaders_dict[phase]):

                #Optimierer initialisieren
                optimizer.zero_grad()

                #Vorwärtsberechnung
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = net(inputs)
                    loss = criterion(outputs, labels)  #Verlust berechnen
                    _, preds = torch.max(outputs, 1)  #Etikett vorhersagen
                    
  
                    #Rückenausbreitung während des Trainings
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                    #Berechnung der Kursivationsergebnisse
                    #Totalverlust aktualisieren
                    epoch_loss += loss.item() * inputs.size(0)  
                    #Die Gesamtzahl der richtigen Antworten wurde aktualisiert
                    epoch_corrects += torch.sum(preds == labels.data)

            #Anzeigeverlust und korrekte Antwortrate für jede Epoche
            epoch_loss = epoch_loss / len(dataloaders_dict[phase].dataset)
            epoch_acc = epoch_corrects.double(
            ) / len(dataloaders_dict[phase].dataset)

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

Ich möchte, dass Sie hier auf ** Kriterien ** achten. Es ist wie folgt als Instanz von nn.CrossEntropyLoss () definiert:

1-3_transfer_learning.ipynb


criterion = nn.CrossEntropyLoss()

Und der Autor behandelt ** Kriterien ** wie eine Funktion.

1-3_transfer_learning.ipynb


loss = criterion(outputs, labels)

Wenn ich jedoch den Quellcode von torch.nn.CrossEntropyLoss überprüfe, gibt es keine Beschreibung der Methode call **! ** Warum können Sie eine Instanz von CrossEntropyLoss () wie eine Funktion behandeln? ** **. Der Zweck dieses Artikels ist es, dieses Rätsel zu lösen. Unter hier erfahren Sie, warum das Vorhandensein oder Fehlen der Methode "call" wichtig ist.

Informationen zur Klassenvererbung

Der Anfang des Quellcodes der "CrossEntropyLoss-Klasse" lautet wie folgt.

Python:torch.nn.modules.loss


class CrossEntropyLoss(_WeightedLoss):

Was bedeutet es zunächst, etwas in Klammern zu setzen, wenn "Klasse" in Python definiert wird? Dies wird als ** Klassenvererbung ** bezeichnet und beim Aufrufen einer Funktion oder Methode verwendet, die in einer anderen Klasse so definiert ist, wie sie ist. (Das folgende spezifische Beispiel wird aus [hier] zitiert (https://murashun.jp/blog/20200113-63.html))

#Erbe
class MyClass:
    def hello(self):
        print("Hello")

class MyClass2(MyClass):
    def world(self):
        print("World")

a = MyClass2()
a.hello() # Hello
a.world() # World

Die Einschränkung hierbei ist, dass die Methode der untergeordneten Klasse überschrieben wird, wenn für eine übergeordnete und eine untergeordnete Klasse eine Methode mit demselben Namen definiert ist. Dies wird als Override bezeichnet.

#überschreiben
class MyClass:
    def hello(self):
        print("Hello")

class MyClass2(MyClass):
    def hello(self):        #Elternklasse Hallo()Methode überschreiben
        print("HELLO")

a = MyClass2()
a.hello()                   # HELLO

Und ich möchte die in der übergeordneten Klasse definierte Methode für die Methode der untergeordneten Klasse verwenden! Sie können die Funktion super () verwenden, wenn Sie darüber nachdenken.

class MyClass1:
    def __init__(self):
       self.val1 = 123

class MyClass2(MyClass1):
    def __init__(self):
        super().__init__()
        self.val2 = 456

a = MyClass2()
print(a.val1) # 123
print(a.val2) # 456

Zurück zur Geschichte: Die "CrossEntropyLoss-Klasse" erbt von der "_WeightedLoss-Klasse". Übrigens, wenn Sie den Code von CrossEntropyLoss etwas genauer überprüfen,

Python:torch.nn.modules.loss


class CrossEntropyLoss(_WeightedLoss):

__constants__ = ['ignore_index', 'reduction']
    ignore_index: int

    def __init__(self, weight: Optional[Tensor] = None, size_average=None, ignore_index: int = -100,
                 reduce=None, reduction: str = 'mean') -> None:
        super(CrossEntropyLoss, self).__init__(weight, size_average, reduce, reduction)
        self.ignore_index = ignore_index

    def forward(self, input: Tensor, target: Tensor) -> Tensor:
        return F.cross_entropy(input, target, weight=self.weight,
                               ignore_index=self.ignore_index, reduction=self.reduction)

Die Beschreibung unterscheidet sich ein wenig vom obigen Beispiel, da sie "super (CrossEntropyLoss, self)" ist, aber [Python offiziell](https://docs.python.org/ja/3/library/functions.html?highlight Wenn Sie sich auf = super # super) beziehen, können Sie sehen, dass die Bedeutungen von beiden genau gleich sind.

Vom Beamten


class C(B):
    def method(self, arg):
        super().method(arg)    # This does the same thing as:
                               # super(C, self).method(arg)

Schauen wir uns nun die Beschreibung der _WeitedLoss-Klasse an.

Python:torch.nn.modules.loss


class _WeightedLoss(_Loss):

Daraus können wir ersehen, dass "_WeitedLoss" von "_Loss" erbt. Schauen wir uns nun die Beschreibung der _WeitedLoss-Klasse an.

Python:torch.nn.modules.loss


class _Loss(Module):

Daraus können wir ersehen, dass _Loss von Module erbt. Werfen wir einen Blick auf die Beschreibung der Modulklasse.

Python:torch.nn.modules.module


class Module:

Modul erbt nichts! Schauen wir uns also den Inhalt von Module an.

torch.nn.Module

Die _Loss-Klasse erbt die __init__-Methode der Modulklasse. Überprüfen Sie dies daher nur. Ich werde versuchen.

Python:torch.nn.modules.module


#Hinweis:Nicht alle Codes sind aufgeführt
from collections import OrderedDict, namedtuple

class Module:
    _version: int = 1

    training: bool

    dump_patches: bool = False
    
    def __init__(self):
        """
        Initializes internal Module state, shared by both nn.Module and ScriptModule.
        """
        torch._C._log_api_usage_once("python.nn_module")

        self.training = True
        self._parameters = OrderedDict()
        self._buffers = OrderedDict()
        self._non_persistent_buffers_set = set()
        self._backward_hooks = OrderedDict()
        self._forward_hooks = OrderedDict()
        self._forward_pre_hooks = OrderedDict()
        self._state_dict_hooks = OrderedDict()
        self._load_state_dict_pre_hooks = OrderedDict()
        self._modules = OrderedDict()

Sie können sehen, dass hier viele "OrderedDict ()" definiert sind. Weitere Informationen zu "OrederedDict ()" finden Sie unter hier. Einfach ausgedrückt, wie der Name schon sagt, ** " Ein leeres Diktat, das bestellt wird **. Mit anderen Worten, diese Klasse definiert nur viele leere Wörterbücher.

Und tatsächlich ist hier die fragliche "call" -Methode definiert!

Python:torch.nn.modules.module


def _call_impl(self, *input, **kwargs):
        for hook in itertools.chain(
                _global_forward_pre_hooks.values(),
                self._forward_pre_hooks.values()):
            result = hook(self, input)
            if result is not None:
                if not isinstance(result, tuple):
                    result = (result,)
                input = result
        if torch._C._get_tracing_state():
            result = self._slow_forward(*input, **kwargs)
        else:
            result = self.forward(*input, **kwargs)
        for hook in itertools.chain(
                _global_forward_hooks.values(),
                self._forward_hooks.values()):
            hook_result = hook(self, input, result)
            if hook_result is not None:
                result = hook_result
        if (len(self._backward_hooks) > 0) or (len(_global_backward_hooks) > 0):
            var = result
            while not isinstance(var, torch.Tensor):
                if isinstance(var, dict):
                    var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
                else:
                    var = var[0]
            grad_fn = var.grad_fn
            if grad_fn is not None:
                for hook in itertools.chain(
                        _global_backward_hooks.values(),
                        self._backward_hooks.values()):
                    wrapper = functools.partial(hook, self)
                    functools.update_wrapper(wrapper, hook)
                    grad_fn.register_hook(wrapper)
        return result

    __call__ : Callable[..., Any] = _call_impl

Die letzte Zeile __call__: Callable [..., Any] = _call_impl setzt den Inhalt von __call__ auf _call_impl. Wenn Sie also die Instanz wie eine Funktion aufrufen, wird die obige Funktion ausgeführt. Wenn Sie die Bedeutung von "Callable [..., Any]" nicht verstehen, können Sie auf [hier] verweisen (https://qiita.com/KtheS/items/7a2bec2a94cf3587df14). Dieser Doppelpunkt ist auch eine Funktionsanmerkung. Weitere Informationen finden Sie unter hier. Einfach ausgedrückt, schreibt es einfach "einen Ausdruck, der als Anmerkung in das Argument oder den Rückgabewert der Funktion dient".

Ich werde der Bedeutung dieses Codes in diesem Artikel folgen.

Darüber hinaus sind einige Methoden in der Modulklasse definiert. Überprüfen Sie daher gegebenenfalls.

Folgendes kann durch Scannen gelesen werden.

torch.nn._Loss

Die _WeightedLoss-Klasse erbt die __init__-Methode der _Loss-Klasse. Ich werde es prüfen.

Python:torch.nn.modules.loss


class _Loss(Module):
reduction: str

    def __init__(self, size_average=None, reduce=None, reduction: str = 'mean') -> None:
        super(_Loss, self).__init__()
        if size_average is not None or reduce is not None:
            self.reduction = _Reduction.legacy_get_string(size_average, reduce)
        else:
            self.reduction = reduction

Hier können Sie sehen, dass wir eine neue "Selbstreduktion" einführen. Und dieser Wert scheint von den Werten von "size_average" und "reduct" abzuhängen.

torch.nn.__WeightedLoss

Die "init" -Methode der _WeightedLoss-Klasse wird von der "CrossEntropyLoss-Klasse" geerbt. Ich werde es prüfen.

Python:torch.nn.modules.loss


class _WeightedLoss(_Loss):
    def __init__(self, weight: Optional[Tensor] = None, size_average=None, reduce=None, reduction: str = 'mean') -> None:
        super(_WeightedLoss, self).__init__(size_average, reduce, reduction)
        self.register_buffer('weight', weight)

Hier wird "Optional [Tensor]" in der Funktionsanmerkung "Gewicht" angegeben. Die Erklärung von hier ist leicht zu verstehen. Einfach ausgedrückt bedeutet "Gewicht", dass entweder "Tensortyp" oder "Keine Typ" eingeschlossen werden kann.

Kommen wir zurück zum Hauptthema. Hier gibt es eine neue Funktion namens "self.register_buffer", eine Funktion, die in der Klasse "Module" definiert ist. Unten ist der Quellcode.

Python:torch.nn.modules.module


forward: Callable[..., Any] = _forward_unimplemented

    def register_buffer(self, name: str, tensor: Optional[Tensor], persistent: bool = True) -> None:
        r"""Adds a buffer to the module.

        This is typically used to register a buffer that should not to be
        considered a model parameter. For example, BatchNorm's ``running_mean``
        is not a parameter, but is part of the module's state. Buffers, by
        default, are persistent and will be saved alongside parameters. This
        behavior can be changed by setting :attr:`persistent` to ``False``. The
        only difference between a persistent buffer and a non-persistent buffer
        is that the latter will not be a part of this module's
        :attr:`state_dict`.

        Buffers can be accessed as attributes using given names.

        Args:
            name (string): name of the buffer. The buffer can be accessed
                from this module using the given name
            tensor (Tensor): buffer to be registered.
            persistent (bool): whether the buffer is part of this module's
                :attr:`state_dict`.

        Example::

            >>> self.register_buffer('running_mean', torch.zeros(num_features))

        """
        if persistent is False and isinstance(self, torch.jit.ScriptModule):
            raise RuntimeError("ScriptModule does not support non-persistent buffers")

        if '_buffers' not in self.__dict__:
            raise AttributeError(
                "cannot assign buffer before Module.__init__() call")
        elif not isinstance(name, torch._six.string_classes):
            raise TypeError("buffer name should be a string. "
                            "Got {}".format(torch.typename(name)))
        elif '.' in name:
            raise KeyError("buffer name can't contain \".\"")
        elif name == '':
            raise KeyError("buffer name can't be empty string \"\"")
        elif hasattr(self, name) and name not in self._buffers:
            raise KeyError("attribute '{}' already exists".format(name))
        elif tensor is not None and not isinstance(tensor, torch.Tensor):
            raise TypeError("cannot assign '{}' object to buffer '{}' "
                            "(torch Tensor or None required)"
                            .format(torch.typename(tensor), name))
        else:
            self._buffers[name] = tensor
            if persistent:
                self._non_persistent_buffers_set.discard(name)
            else:
                self._non_persistent_buffers_set.add(name)

Es ist ein ziemlich langer Code, aber die obere Hälfte ist die Erklärung des Codes, und der Teil über dem "else" der "if-Anweisung" ist nur die Fehlereinstellung, daher wird die Erklärung weggelassen. Und in "else" setzen Sie Elemente in "self._buffers" vom Typ "dict". Mit anderen Worten, durch Definieren der WeightedLoss-Klasse haben wir:

self._buffer = {'weight': weight} #Das Gewicht rechts ist vom Typ Tensor oder vom Typ None

torch.nn.CrossEntropyLoss Schließlich bin ich auf die Frage zurückgekommen. Unten ist der Quellcode. Es gibt einen langen Kommentar, aber ich werde sie alle zitieren.

Python:torch.nn.modules.loss


class CrossEntropyLoss(_WeightedLoss):
    r"""This criterion combines :func:`nn.LogSoftmax` and :func:`nn.NLLLoss` in one single class.

    It is useful when training a classification problem with `C` classes.
    If provided, the optional argument :attr:`weight` should be a 1D `Tensor`
    assigning weight to each of the classes.
    This is particularly useful when you have an unbalanced training set.

    The `input` is expected to contain raw, unnormalized scores for each class.

    `input` has to be a Tensor of size either :math:`(minibatch, C)` or
    :math:`(minibatch, C, d_1, d_2, ..., d_K)`
    with :math:`K \geq 1` for the `K`-dimensional case (described later).

    This criterion expects a class index in the range :math:`[0, C-1]` as the
    `target` for each value of a 1D tensor of size `minibatch`; if `ignore_index`
    is specified, this criterion also accepts this class index (this index may not
    necessarily be in the class range).

    The loss can be described as:

    .. math::
        \text{loss}(x, class) = -\log\left(\frac{\exp(x[class])}{\sum_j \exp(x[j])}\right)
                       = -x[class] + \log\left(\sum_j \exp(x[j])\right)

    or in the case of the :attr:`weight` argument being specified:

    .. math::
        \text{loss}(x, class) = weight[class] \left(-x[class] + \log\left(\sum_j \exp(x[j])\right)\right)

    The losses are averaged across observations for each minibatch. If the
    :attr:`weight` argument is specified then this is a weighted average:

    .. math::
        \text{loss} = \frac{\sum^{N}_{i=1} loss(i, class[i])}{\sum^{N}_{i=1} weight[class[i]]}

    Can also be used for higher dimension inputs, such as 2D images, by providing
    an input of size :math:`(minibatch, C, d_1, d_2, ..., d_K)` with :math:`K \geq 1`,
    where :math:`K` is the number of dimensions, and a target of appropriate shape
    (see below).


    Args:
        weight (Tensor, optional): a manual rescaling weight given to each class.
            If given, has to be a Tensor of size `C`
        size_average (bool, optional): Deprecated (see :attr:`reduction`). By default,
            the losses are averaged over each loss element in the batch. Note that for
            some losses, there are multiple elements per sample. If the field :attr:`size_average`
            is set to ``False``, the losses are instead summed for each minibatch. Ignored
            when reduce is ``False``. Default: ``True``
        ignore_index (int, optional): Specifies a target value that is ignored
            and does not contribute to the input gradient. When :attr:`size_average` is
            ``True``, the loss is averaged over non-ignored targets.
        reduce (bool, optional): Deprecated (see :attr:`reduction`). By default, the
            losses are averaged or summed over observations for each minibatch depending
            on :attr:`size_average`. When :attr:`reduce` is ``False``, returns a loss per
            batch element instead and ignores :attr:`size_average`. Default: ``True``
        reduction (string, optional): Specifies the reduction to apply to the output:
            ``'none'`` | ``'mean'`` | ``'sum'``. ``'none'``: no reduction will
            be applied, ``'mean'``: the weighted mean of the output is taken,
            ``'sum'``: the output will be summed. Note: :attr:`size_average`
            and :attr:`reduce` are in the process of being deprecated, and in
            the meantime, specifying either of those two args will override
            :attr:`reduction`. Default: ``'mean'``

    Shape:
        - Input: :math:`(N, C)` where `C = number of classes`, or
          :math:`(N, C, d_1, d_2, ..., d_K)` with :math:`K \geq 1`
          in the case of `K`-dimensional loss.
        - Target: :math:`(N)` where each value is :math:`0 \leq \text{targets}[i] \leq C-1`, or
          :math:`(N, d_1, d_2, ..., d_K)` with :math:`K \geq 1` in the case of
          K-dimensional loss.
        - Output: scalar.
          If :attr:`reduction` is ``'none'``, then the same size as the target:
          :math:`(N)`, or
          :math:`(N, d_1, d_2, ..., d_K)` with :math:`K \geq 1` in the case
          of K-dimensional loss.

    Examples::

        >>> loss = nn.CrossEntropyLoss()
        >>> input = torch.randn(3, 5, requires_grad=True)
        >>> target = torch.empty(3, dtype=torch.long).random_(5)
        >>> output = loss(input, target)
        >>> output.backward()
    """
    __constants__ = ['ignore_index', 'reduction']
    ignore_index: int

    def __init__(self, weight: Optional[Tensor] = None, size_average=None, ignore_index: int = -100,
                 reduce=None, reduction: str = 'mean') -> None:
        super(CrossEntropyLoss, self).__init__(weight, size_average, reduce, reduction)
        self.ignore_index = ignore_index

    def forward(self, input: Tensor, target: Tensor) -> Tensor:
        return F.cross_entropy(input, target, weight=self.weight,
                               ignore_index=self.ignore_index, reduction=self.reduction)

Zunächst wurde in der Methode init eine neue Variable namens self.ignore_index hinzugefügt. Außerdem wird eine Funktion namens "forward ()" definiert. Die __call__-Methode wurde jedoch seit der Modulklasse nicht mehr definiert. Daher war die __call__-Methode der Modulklasse die Identität, dass die Instanz der CrossEntropyLoss-Klasse wie eine Funktion verwendet wurde.

In diesem Artikel möchte ich genauer untersuchen, was passiert, wenn Sie eine Instanz von CrossEntropyLoss () wie eine Funktion behandeln!

Recommended Posts

[PyTorch] Warum Sie eine Instanz von CrossEntropyLoss () wie eine Funktion behandeln können
[PyTorch] Ein wenig Verständnis von CrossEntropyLoss mit mathematischen Formeln
Erstellen Sie eine Instanz einer vordefinierten Klasse aus einer Zeichenfolge in Python
[Road to Intermediate Python] Rufen Sie eine Klasseninstanz wie eine Funktion mit __call__ auf
Wenn Sie eine Liste mit dem Standardargument der Funktion angeben ...
Verwendung von Lambda (beim Übergeben einer Funktion als Argument einer anderen Funktion)