Je veux colorier des photos en noir et blanc de souvenirs avec GAN

Motivation

C'est un thème souvent lié à l'apprentissage automatique, mais j'aimerais coloriser les photographies en noir et blanc. J'ai également mis le code sur GitHub. [GitHub]

L'autre jour, alors que je regardais les reliques de mon père, j'ai soudainement vu une vieille photo en noir et blanc. Je l'ai laissé tel quel quand je suis mort, mais maintenant je pense qu'il peut être coloré. Cela ressemble plus à un blog de passe-temps personnel qu'à un blog technique. En outre, il existe des blogs et des livres de ceux qui sont mentionnés lors de l'étude du GAN. L'article de cette personne est très éducatif, et cet article a également du contenu. Je publierai un lien ci-dessous, alors jetez un œil.

[URL de référence] Shikoan's ML Blog Apprendre de la suppression de la mosaïque, apprentissage en profondeur de pointe

Structure du blog

Présentation de pix2 pix

GAN Discriminator est intégré à Generator, et les deux sont appris en parallèle. C'est une image de l'ajout d'une fonction de perte dynamique en utilisant la valeur prédite de Discriminateur à Perte de générateur. Il existe différents types de GAN, et la division varie en fonction de la définition, mais il existe les divisions suivantes.

type Aperçu
Conditional GAN Il existe une relation entre l'entrée et la sortie du générateur
Non-Conditional GAN L'entrée et la sortie du générateur ne sont pas liées

Cette fois, pix2pix est un GAN conditionnel (CGAN) typique. DCGAN, qui est souvent utilisé dans les didacticiels GAN, génère une sortie à partir de données de bruit, il devient donc un GAN non conditionnel. Puisque pix2pix est CGAN, les informations d'entrée deviennent très importantes. Par exemple, dans DCGAN, l'apprentissage procède en utilisant la perte Adversarial qui apparaît en relation avec Discriminator, mais dans le cas de pix2pix, en plus de Adversarial Loss, la différence entre la fausse image et l'image réelle (par exemple, la perte L1 ou MSE) est également utilisée. L'apprentissage progresse. Ce faisant, l'apprentissage progresse plus rapidement que les autres GAN et les résultats sont plus stables. Au contraire, dans le cas d'une méthode d'apprentissage utilisant uniquement la perte L1, afin de réduire la perte, la sortie est vague dans son ensemble, ou le tout est peint solide avec des pixels moyens. Cela a tendance à donner l'impression d'être bâclé, et en ajoutant une perte d'adversaire, l'apprentissage a tendance à progresser de sorte que même si la perte de L1 peut être assez importante, une image plus réaliste peut être produite. Le principe selon lequel il y a un compromis entre la qualité perçue et la distorsion est l'un des facteurs très importants dans l'examen du GAN.

Référence: The Perception-Distortion Tradeoff (2017) [arXiv]

PatchGAN Ce qui suit est le papier original de pix2pix, mais je pense qu'il vaut mieux se référer ici pour plus de détails sur PatchGAN.

Image-to-Image Translation with Conditional Adversarial Networks (2016) [arXiv]

Dans PatchGAN, lorsque Discriminator juge l'exactitude d'une image, celle-ci est divisée en plusieurs zones et le jugement d'exactitude est effectué dans chaque zone.

france.jpg

スクリーンショット 2020-05-26 15.57.00.png

Diviser la zone ne signifie pas que vous divisez réellement l'image et la plongez séparément dans le Discriminator. En théorie, c'est le cas, mais en termes de mise en œuvre, une image est insérée dans le Discriminator et sa sortie est transformée en un tenseur au deuxième étage. À ce moment-là, la valeur de chaque pixel du tenseur est dérivée sur la base des informations de la zone de patch de l'image d'entrée, et par conséquent, la valeur de chaque pixel du tenseur se situe entre le vrai vrai ou faux (1 ou 0). Le patch GAN est réalisé en prenant la perte de. Je ne pense pas que ce soit facile à expliquer avec des mots, je vais donc donner un exemple avec le drapeau français ci-dessus.


fig, axes = plt.subplots(1,2)

#Chargement des images
img = cv2.imread('france.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (600, 300))   #format original-> (300, 600, 3)
axes[0].imshow(img)
axes[0].set_xticks([])
axes[0].set_yticks([])

img = ToTensor()(img)   # (300, 600, 3) -> (3, 300, 600)
img = img.view(1, 3, 300, 600)   # (3, 300, 600) -> (1, 3, 300, 600)
img = nn.AvgPool2d(100)(img)   # (1, 3, 300, 600) -> (1, 3, 3, 6)
img = nn.Conv2d(3, 1, kernel_size=1)(img)   # (1, 3, 3, 6) -> (1, 1, 3, 6)
img = np.squeeze(img.detach().numpy())

axes[1].imshow(img, cmap='gray')
axes[1].set_xticks([])
axes[1].set_yticks([])

[résultat]

download.png

Dans ce qui précède, l'image d'entrée est déposée dans une carte de caractéristiques de taille (3, 6), mais il ne s'agit que de compresser toute la zone de patch de l'image d'origine en (1, 1). Dans l'exemple ci-dessus, l'authenticité est évaluée pour `` 3 × 6 = 18 '' pixels.

Unet pix2pix utilise U-net comme générateur. U-net, qui est le même dans la segmentation, a une connexion de saut en plus de la structure encodeur-décodeur, de sorte que les informations des données d'entrée ne soient pas perdues autant que possible. Voici la méthode avancée de Generator utilisée dans l'expérience, mais vous pouvez voir que les informations d'origine sont concaténées à chaque fois avec torch.cat.

    def forward(self, x):
        x1 = self.enc1(x)
        x2 = self.enc2(x1)
        x3 = self.enc3(x2)
        x4 = self.enc4(x3)
        out = self.dec1(x4)
        out = self.dec2(torch.cat([out, x3], dim=1))
        out = self.dec3(torch.cat([out, x2], dim=1))
        out = self.dec4(torch.cat([out, x1], dim=1))
        return out

À propos, l'image U-net suivante est une diapositive d'image liée à l'apprentissage automatique publiée gratuitement par Google, qui a récemment été mentionnée sur Twitter. [Visuels ML] Il y a beaucoup de belles photos, alors jetez un œil.

ML Visuals by dair.ai.jpg

À propos de cet ensemble de données

Google PhotoScan Utilisez MIT-Adobe FiveK Dataset comme données d'entraînement. Ceci est souvent utilisé dans les papiers d'amélioration d'image, et il s'agit d'un ensemble d'images non traitées et d'images post-traitées par un éditeur professionnel, mais cette fois, j'utiliserai cette image traitée. En ce qui concerne les photos traitées, il existe de nombreuses photos avec des couleurs plus vives que les photos couleur ordinaires, j'ai donc pensé que cela conviendrait également à cette tâche. Si la taille des données est petite, la capacité de données n'est pas grande et le téléchargement est raisonnable dans une certaine mesure. Il s'agit d'une vraie photo en noir et blanc, mais elle a été convertie en données à l'aide d'une application appelée "Google PhotoScan" sur l'iPhone. Cette application sortira en détail si vous la recherchez sur Google, mais elle est assez excellente et elle la convertira en jolies données sans aller dans un magasin photo (et en un instant). La photo originale était assez ancienne et jaunie, mais lorsque je l'ai convertie en une image en noir et blanc, elle n'avait pas l'air très différente d'une image normale en noir et blanc.

Taille de l'image

L'image en cinqk et l'image que vous souhaitez coloriser ne sont pas carrées mais rectangulaires, et les proportions sont différentes. Par conséquent, j'ai décidé d'adopter l'un des quatre modèles suivants.

  1. Redimensionner en un carré uniforme
  2. Redimensionnez le carré sans modifier le rapport hauteur / largeur d'origine et remplissez la zone vide de noir.
  3. Placez Reisize dans un carré sans changer le rapport hauteur / largeur d'origine et placez les informations de l'image d'origine dans la zone vide.
  4. Faites pivoter l'image portrait, convertissez-la en paysage, puis redimensionnez-la à la même taille rectangulaire.
  5. Recadrer en carré

Pour le moment, j'ai décidé d'utiliser la méthode 3 cette fois. En ce qui concerne 1 et 4, la raison de l'adoption est qu'il est possible que les caractéristiques de l'image changent de manière significative, et pour 2 et 3, j'ai pensé que 3 aurait plus d'informations. Bien sûr, si vous allez tout droit, c'est peut-être le numéro 5, mais je n'aime pas les cultures carrées avant les photos de production. Le résultat de la sortie sera rogné au rapport hauteur / largeur d'origine lors du post-traitement.

スクリーンショット 2020-05-26 17.43.00.png

Commencer à apprendre

Partie 1

Apprentissage partie 1

bce_loss.png l1_loss.png

Vous pouvez voir comment le discriminateur devient progressivement plus fort et la perte du générateur augmente, à partir de l'endroit où il a tendance à se déchaîner dans les premiers stades. Quant à la perte de L1, je ne sais pas si elle diminue. (La difficulté de l'évaluation du GAN est que `L1loss est petit = proche de la couleur réelle ≠ réelle".)

Partie 2

Apprentissage de la partie 2. Dans l'apprentissage 1, le discriminateur avait tendance à être plus fort, j'ai donc décidé d'expérimenter en modifiant les points suivants.

- D,Et la perte contradictoire de G est passée de BCE à Hinge Loss
-Changer la normalisation des lots de D en normalisation d'instance
-Réduire de moitié la fréquence de mise à jour du poids de D (moitié de G)

Perte d'entropie croisée et perte de charnière

Dans la partie 2, le changement par rapport à la partie 1 consiste à changer la fonction de perte de l'entropie croisée binaire utilisée dans la partie 1 à la perte de charnière. En gros, le but est de «faire une faible perte pour éviter qu'un réseau (D ou G) ne devienne trop fort». Ce qui suit est l'entropie croisée (version D) de la plaque de fer.

D:loss = -\sum_{i=1}^{n}\bigl(t_ilogy_i - (1-t_i) log(1-y_i) \bigl)

En bref, lorsque target = 1, la sortie doit être aussi élevée que possible (puisque la couche finale est sigmoïde, plus elle est grande, plus elle est proche de 1). Lorsque target = 0, si vous vous entraînez à augmenter la sortie dans le sens négatif, la perte sera plus petite.

Au contraire, pour G, nous nous entraînerons à maximiser l'équation ci-dessus. De plus, dans le cas de G, seule la fausse image générée par elle-même est évaluée, donc le terme gauche de la formule ci-dessus disparaît et cela devient plus simple.

G:loss = \sum_{i=1}^{n}log\Bigl(1 - D\bigl(G(z_i))\bigl) \Bigl)(Maximiser)\\
= \sum_{i=1}^{n}log\Bigl(D\bigl(G(z_i)) -  1\bigl) \Bigl)(Minimiser)\\
=  -\sum_{i=1}^{n}log\Bigl(D\bigl(G(z_i))\bigl) \Bigl) (Il est également possible de penser que cela est minimisé)

Ce qui précède donne deux modèles d'optimisation, mais il semble qu'il existe les deux méthodes de mise en œuvre.

Par contre, concernant la perte de charnière, je pense que ce site est facile à comprendre. [Référence] Le site dit qu'il n'est pas souvent utilisé pour autre chose que SVM, mais il est intéressant de noter qu'il est actuellement utilisé dans d'autres méthodes. En entropie croisée, la cible est exprimée par (0,1), mais en charnière, elle est exprimée par (-1,1).

t = ±1 \\
Loss = max(0, 1-t*y)

En ce qui concerne la formule, si vous produisez une sortie plus petite lorsque target = -1, et inversement, produisez une sortie plus grande lorsque cible = 1, la perte sera faible. Cependant, contrairement à l'entropie croisée, vous pouvez également voir que la perte est coupée à 0 dans une certaine mesure. Dans le cas de l'entropie croisée, la perte ne disparaîtra jamais à moins qu'elle ne soit complètement prédite par (0,1), mais ce n'est pas le cas avec les charnières. C'est la raison pour laquelle on l'appelle `` faible perte ''. L'implémentation de PyTorch est la suivante. Au niveau des charnières, il est difficile de diviser les motifs par D et G, donc je pense qu'il vaut mieux les classer tous ensemble.

# ones:Patch avec toutes les valeurs 1
# zeros:Patch avec toutes les valeurs 0

# Gloss(BCE)
loss = torch.nn.BCEWithLogitsLoss()(d_out_fake, ones)

# Gloss(Hinge)
loss = -1 * torch.mean(d_out_fake)

# Dloss(BCE)
loss_real = torch.nn.BCEWithLogitsLoss()(d_out_real, ones)
loss_fake = torch.nn.BCEWithLogitsLoss()(d_out_fake, zeros)
loss = loss_reak + loss_fake

# Dloss(Hinge)
loss_real = -1 * torch.mean(torch.min(d_out_real-1, zeros))
loss_fake = -1 * torch.mean(torch.min(-d_out_fake-1, zeros))
loss = loss_reak + loss_fake

Instance Normalization La normalisation d'instance est un dérivé de la normalisation par lots. Le contenu, y compris la normalisation par lots, est bien organisé dans les articles suivants.

[GIF] Explication de CNN pour les débutants sur la normalisation par lots et ses amis

La normalisation par lots effectue un traitement de standardisation entre les mêmes canaux de données contenus dans un mini-lot, mais la normalisation d'instance n'effectue pas le mini-lot entier mais les données seules. Le point est la normalisation des lots avec une taille de lot 1. Par exemple, il est également utilisé dans pix2pix HD, qui est un dérivé de pix2pix, mais son but est de rendre difficile la convergence de l'apprentissage en supprimant l'augmentation du gradient. L'objectif principal est d'équilibrer D et G en appliquant cela à D.

Voici les résultats. Vous pouvez voir que la convergence de Discriminator est clairement plus lente qu'auparavant. Je pense également que la diminution de la perte de L1 est plus importante qu'avant. adv_loss.png l1_loss.png

Partie 3

La partie d'apprentissage 3 a changé les points suivants de l'apprentissage 1.

-Suivre essentiellement l'apprentissage 1
-Alignez le taux d'apprentissage sur le papier d'origine.(1e-4 -> 2e-4)
-Suppression de l'ajustement du taux d'apprentissage à l'aide du planificateur
-Changement de la taille de l'image de 320 à 256 (suivant le papier)
-Changement du nombre de zones PatchGAN de 10x10 à 4x4
-Suppression du flou dans l'augmentation du train

Cependant, le résultat était presque le même que 1.

Partie 4

En regardant les résultats jusqu'à présent, nous pouvons voir la tendance selon laquelle «les paysages naturels peuvent être relativement colorés, mais les gens ne travaillent pas du tout». Cela n'a aucun sens pour l'objectif initial, j'ai donc décidé d'acquérir à nouveau les données moi-même. Quant à savoir comment collecter des données, j'ai écrit dans l'article précédent de Qiita, mais j'ai utilisé BingImageSearch. [Lien de l'article Qiita] De plus, bien qu'il soit basé sur la partie 2, il a été légèrement altéré. De plus, comme le nombre de données a triplé (plus de 13 000), nous avons réduit de 200 à 100 époques.

#Changements par rapport à l'apprentissage 1
-Ajouter des données d'entraînement (cinqk)-> fivek+Image de personnes)
-200 numéros d'époque-> 97
- D,Changement de la perte conflictuelle de BCE en perte charnière(Avec la partie 2)
-Changer la normalisation des lots de D en normalisation d'instance(Avec la partie 2)
-Réduire de moitié la fréquence de mise à jour du poids de D(Avec la partie 2)
-Suppression de l'ajustement du taux d'apprentissage à l'aide du planificateur(Avec la partie 3)
-Taux d'apprentissage 1e-4->2e-Changé en 4 (avec la partie 3)

#La raison pour laquelle le nombre de pixels dans la sortie était un peu insatisfaisant n'est peut-être pas vraiment bonne
-Changer la taille de l'image(320->352) (<-new)
-Parallèlement à ce qui précède, le nombre de zones PatchGAN(10,10)->(11,11)changer en (<-new)

Voici les résultats. Par rapport à la partie 2, le grand mouvement vertical dans la dernière étape semble être la première cause de suppression du planificateur. adv_loss.png l1_loss.png

Résumé. Et le résultat de la photo importante

Grâce à divers essais et erreurs, les images de paysage peuvent être colorées sans gêne à une vitesse considérable, tandis que les images de portrait et les images de contraste flashy (par exemple, des personnes, des vêtements, des fleurs, des objets artificiels, etc.) ne sont pas aussi colorées. J'ai trouvé une tendance à ne pas continuer.

(Gauche: fausse image Droite: image réelle) 000062.png 000133.png 000200.png 000219.png 000314.png 000324.png 000331.png 000372.png 000552.png 000553.png 000684.png 000758.png 000771.png 000909.png 001081.png 001179.png 001242.png 001494.png 001554.png 002079.png 002330.png 002377.png 002480.png Au contraire, je sens que cela semble mystérieux. C'est intéressant à regarder. De plus, il me semblait que ma photo de la production réelle était bien colorée (rires). .. ..

(Finalement considéré) Bien que ce soit mon image de test (photo du père), le résultat est plus inégal que les données d'apprentissage et de vérification. Probablement, je pense qu'il y a probablement un problème de résolution en premier lieu. L'image ci-dessus est toujours bonne, et même si d'autres images en noir et blanc sont converties en données, le contour peut être vu lorsqu'il est agrandi, mais la résolution est inégale dans de petites zones et le résultat est décevant. Donc, si je veux le faire sérieusement, je ferai la tâche de super-résolution en parallèle. Alternativement, il peut être nécessaire de prendre des mesures telles que le brouillage des données d'apprentissage. Je le ferai la prochaine fois (prévu).

Recommended Posts

Je veux colorier des photos en noir et blanc de souvenirs avec GAN
Je veux faire ○○ avec les Pandas
Je veux déboguer avec Python
Je veux sortir le début du mois prochain avec Python
Je veux vérifier la position de mon visage avec OpenCV!
Je veux détecter des objets avec OpenCV
Je veux écrire un blog avec Jupyter Notebook
Je veux installer Python avec PythonAnywhere
Je veux analyser les journaux avec Python
Je veux jouer avec aws avec python
Je veux exprimer mes sentiments avec les paroles de Mr. Children
Je veux colorer une partie de la chaîne Excel avec Python
Je souhaite arrêter la suppression automatique de la zone tmp dans RHEL7
Je veux utiliser MATLAB feval avec python
J'ai essayé de déplacer GAN (mnist) avec keras
Je veux moquer datetime.datetime.now () même avec pytest!
Je souhaite afficher plusieurs images avec matplotlib.
Je veux frapper 100 sciences des données avec Colaboratory
Je veux faire un jeu avec Python
Je veux être OREMO avec setParam!
Je souhaite utiliser le répertoire temporaire avec Python2
Je veux obtenir les données de League of Legends ③
Je veux obtenir les données de League of Legends ②
Je ne veux pas utiliser -inf avec np.log
#Unresolved Je veux compiler gobject-introspection avec Python3
Je souhaite utiliser ip vrf avec SONiC
Je veux résoudre APG4b avec Python (chapitre 2)
Je veux recommencer avec Migrate de Django
Je souhaite personnaliser l'apparence de zabbix
Je veux obtenir les données de League of Legends ①
Je veux écrire dans un fichier avec Python
Je souhaite afficher uniquement différentes lignes du fichier texte avec diff
Je souhaite envoyer Gmail avec Python, mais je ne peux pas en raison d'une erreur
Je veux convertir une image en WebP avec sucette
Je souhaite détecter une connexion non autorisée à Facebook avec Jubatus (1)
J'ai essayé d'extraire des fonctionnalités avec SIFT d'OpenCV
Je veux faire la transition avec un bouton sur le ballon
Je veux grep le résultat de l'exécution de strace
Je veux gérer l'optimisation avec python et cplex
Je veux escalader une montagne avec l'apprentissage par renforcement
Je veux hériter de l'arrière avec la classe de données python
Je veux bien comprendre les bases de Bokeh
Je veux travailler avec un robot en python.
Je veux diviser une chaîne de caractères avec hiragana
Je souhaite installer un package de Php Redis
Je veux AWS Lambda avec Python sur Mac!
Je souhaite créer manuellement une légende avec matplotlib
[TensorFlow] Je souhaite traiter des fenêtres avec Ragged Tensor
Je veux faire fonctionner un ordinateur quantique avec Python
Je veux lier une variable locale avec lambda
Je souhaite augmenter la sécurité de la connexion SSH
Je veux tracer les informations de localisation de GTFS Realtime sur Jupyter! (Avec ballon)
J'ai essayé de trouver l'entropie de l'image avec python
Je veux pouvoir analyser des données avec Python (partie 3)
Je souhaite supprimer l'avertissement d'importation non résolue de Python avec vsCode
Je souhaite utiliser facilement les fonctions R avec le notebook ipython
J'ai essayé de trouver la moyenne de plusieurs colonnes avec TensorFlow
Je veux pouvoir analyser des données avec Python (partie 1)
Je veux créer un éditeur de blog avec l'administrateur de django