Answers.Body: <p>This question turned out to be more interesting than I thought. Basically I think that the problem is too simple for CNNs, and any random-looking feature extraction gets it done.</p>
  <p>To get the model to converge to a structure I was expecting, I had to make the network very simple, and use a specific initialization. First of all, here is the Keras code:</p>
  <pre><code>model = tf_models.Sequential([
      l.InputLayer(X.shape[1:]),
      l.Conv2D(7, 9, activation='relu', kernel_initializer=keras.initializers.Constant(init)),
      l.BatchNormalization(),
      l.GlobalMaxPooling2D(),
      l.Dense(1, activation='sigmoid', kernel_initializer='zeros', use_bias=False)
  ])
  </code></pre>
  <p>The generated data looks like this (vertical slopes very tricky, because I used the formula <span class="math-container">$f(x,y) \approx 0$</span>), each input is a 128 x 128 single-channel image:</p>
  <p><a href="https://i.sstatic.net/eAPpg8wv.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/eAPpg8wv.png" alt="generated data" /></a></p>
  <p>The last layer's weights were initialized to zero, here is how they (along with loss and val_loss) changed trough-out the training procedure:</p>
  <p><a href="https://i.sstatic.net/Jpk6nSp2.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/Jpk6nSp2.png" alt="model weights" /></a></p>
  <p>So some Conv2D kernels learned to indicate a gaussian curve, and others indicate the pulse.</p>
  <p>Here is the interesting part: Conv2D weights are initialized to &quot;linear&quot; gradients, to seven different directions. They are shown on the first column of this image. Subsequent columns show how the weights changed trough-out the training procedure. Final weights are shown on y-axis labels. Negative weights detect gaussians and positive ones detect pulses.</p>
  <p><a href="https://i.sstatic.net/Ugg6NCED.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/Ugg6NCED.png" alt="Conv2D weights" /></a></p>
  <p>The pattern becomes a bit more clear if we sort the kernels by their corresponding dense layer's weights:</p>
  <p><a href="https://i.sstatic.net/CbKVK7Pr.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/CbKVK7Pr.png" alt="Conv2D weights sorted" /></a></p>
  <p>So gradients on roughly 45 degree angles didn't change much (they are already good detectors for pulse's specific parts), while vertical gradients become &quot;hard&quot; vertical edge detectors.</p>
  <p>If instead I use random initialization for Conv2D, I don't get so clear patterns or conclusions. On one instance I got five detectors of vertical lines, and two almost identical detectors for -45 degree slopes:</p>
  <p><a href="https://i.sstatic.net/e8syRlhv.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/e8syRlhv.png" alt="Random weights" /></a></p>
  <p>For what it is worth, I got a model with only seven parameters to learn greater than 99.9% train and validation accuracy.</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2024-08-10T14:24:49.563Z
Answers.LastActivityDate: 2024-08-10T14:24:49.563Z
Answers.UserDisplayName: NikoNyrh
Body: <p>I saw <a href="https://ai.stackexchange.com/questions/36263/learned-kernels-in-cnn-seem-just-random-patterns">this question</a> and am having the same issue: kernels on a trained CNN look random.</p>
  <p>I am using Pytorch to train a CNN, and based my code on examples that train the MNIST characters: ~50 convolutional layers of 20x20 kernels, that feed two fully connected layers, with the SGD optimizer, and the MSE or Cross Entropy loss function.</p>
  <p>This is just a learning experiment (learning Pytorch for ~3 months now). I am interested in examining the kernels when a sufficiently low loss and high accuracy is reached.  I am training the network on 100 100x100 grayscale images that contain round or square pulses like these</p>
  <p><a href="https://i.sstatic.net/fVjqSA6t.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/fVjqSA6t.png" alt="a square pulse" /></a>
  or
  <a href="https://i.sstatic.net/IxeaFuCW.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/IxeaFuCW.png" alt="a round pulse" /></a></p>
  <p>The output is a 3 neuron layer that can be 000=round, 001=square, etc.  In all data pairs, 1 is mapped to 0.8 and 0 to 0.1.</p>
  <p>As training proceeds, I was hoping to watch the kernels evolve from random to forms containing corners, lines and slopes; in other words, general features of my data.  Here's what I see:</p>
  <p>If I choose a learning rate that gives finite, numerical loss values (lr=0.01, etc.) the kernels look like this</p>
  <p><a href="https://i.sstatic.net/8VPK55TK.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/8VPK55TK.png" alt="random cnn kernels" /></a></p>
  <p>and do not change at all over 1000s of epochs, even though the loss can get arbitrarily low (1e-10 if I want), with 90%+ accuracy is classifying the pulses.</p>
  <p>If I choose a large learning rate, like (lr=2 or lr=5), I get nan values for the learning rate, but neat looking kernels like this:</p>
  <p><a href="https://i.sstatic.net/XygmVlcg.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/XygmVlcg.png" alt="structured cnn kernels" /></a></p>
  <p>but also do not change at all over 1000s of epochs.</p>
  <p>Am I wrong to expect the kernels to evolve from random to containing features of the training set as training proceeds?  I see no evolution of the kernels at all.</p>
  <p>Thanks for any input. I am hoping features seen in kernels of trained networks might help those in the basic sciences (physics, chem, etc.) with new perspectives on their data.</p>
  <p><strong>Edit a day later:</strong></p>
  <p>I kept working on it, and normalizing the output (using Torch's normalize) seems to help. Watch this:
  <a href="https://i.sstatic.net/9nbqO92K.gif" rel="nofollow noreferrer"><img src="https://i.sstatic.net/9nbqO92K.gif" alt="enter image description here" /></a></p>
  <p><strong>Edit on 12-Aug-2024, in response to @NikoNyrh's comment:</strong></p>
  <p>Dear @NikoNyrh: Thanks for your input and for sharing your fine results. I've kept at it on my end, and was able to find some similar things as you. In particular:</p>
  <ul>
  <li><p>I agree my input figures are simple (maybe too simple). If I use 30+ kernels (as per a typical MNIST model), the kernels stay random looking, even though the loss lowers and converges. As you indicated, this number of kernels with the relatively simple inputs may be too many parameters, making a loss minimum easy for the network to find, without revealing any structure in the kernels.</p>
  </li>
  <li><p>I decided to restrict the network some, cutting the kernels down to 5 (20x20 kernels on 500x500 images), to force more a more selective minimum.  For more diversity in the images, I allow the waveforms to appear at any position in the image (even if it gets cut off by the image boundary).</p>
  </li>
  <li><p>I reworked my training plan. I got access to a GPU and trained on 10,000 images and look for accuracy on 100 images not in in the training set. I get about 17s/epoch on a 4090 consuming 71 Watts.</p>
  </li>
  <li><p>I too achieve 99%+ accuracy with the 100 test images.</p>
  </li>
  <li><p>I found the network very finicky to train, but this seems to work: L1Loss, Tanh activation on the fully connected layers, lr=0.1, momentum=0.0, batch_size=1,000 (on the 10,000 input images), and very selective DropOut.</p>
  </li>
  <li><p>My network is huge (200M parameters) as shown here:</p>
  </li>
  </ul>
  <pre><code>    class neural_net(nn.Module):
          def __init__(self):
              super(neural_net, self).__init__()
              self.conv_layer_count = 5
              self.K = 25
      
              self.conv1 = nn.Conv2d(in_channels=1, out_channels=self.conv_layer_count, kernel_size=self.K, stride=1, padding=0)
              self.relu = nn.ReLU()
              self.mp1 =  nn.MaxPool2d(2)
              self.conv2 = nn.Conv2d(in_channels=self.conv_layer_count, out_channels=1, kernel_size=self.K)
              self.act = nn.Tanh()
              self.dropout1 = nn.Dropout(0.01)
              self.fc1 = nn.Linear(226*226, 4096)
              self.fc2 = nn.Linear(4096, 1024)
              self.fc3 = nn.Linear(1024, 3)
      
      
          def forward(self, x):
              x = self.conv1(x)
              x = self.relu(x)
      
              x = self.conv2(x)
              x = self.relu(x)
              x = self.mp1(x)
      
        
              x = self.dropout1(x)
      
              x = torch.flatten(x, 1)
              x = self.fc1(x)
              x = self.relu(x)
      
              x = self.fc2(x)
              x = self.act(x)
      
              x = self.fc3(x)
              x = self.act(x)
      
              return x
  </code></pre>
  <ul>
  <li>The Loss profile is interesting, with all kinds of ups and downs on the way to convergence.</li>
  </ul>
  <p><a href="https://i.sstatic.net/65IQH3VB.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/65IQH3VB.png" alt="loss profile for this CNN" /></a></p>
  <ul>
  <li>Here are some of my eventual kernels that like yours, show the vertical and horizontal lines, including some 45 degree slopes, and wisps of other curves as well.</li>
  </ul>
  <p><a href="https://i.sstatic.net/4aBuHqzL.png" rel="nofollow noreferrer"><img src="https://i.sstatic.net/4aBuHqzL.png" alt="eventual kernels for the pulses" /></a></p>
  <ul>
  <li>Thanks again @NikoNyrh!! Very interesting results.</li>
  </ul>
  
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2024-07-11T22:54:44.333Z
Comments.Text: Neat GIF.  You mentioned before the Edit that you saw "no evolution of the kernels at all."  Can you say more about how you normalized the output to achieve the evolution visible in the GIF?
Comments.UserDisplayName: SapereAude
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2024-07-12T02:03:28.313Z
Comments.Text: @SapereAude: In my forward() function, I literally added `x=F.normalize(x)`, where x is the vector coming off of the activation of the last and fully connected layer in the network.  `x` out of the normalization is then returned by forward().
Comments.UserDisplayName: Tom Bensky
Tags: <convolutional-neural-networks><filters>
Title: Kernels on a trained CNN seem random
UserDisplayName: Tom Bensky
ThreadText: # Kernels on a trained CNN seem random
  
  (Asked by: username_0 on 2024-07-08)
  
  I saw this question and am having the same issue: kernels on a trained CNN look random.
  I am using Pytorch to train a CNN, and based my code on examples that train the MNIST characters: ~50 convolutional layers of 20x20 kernels, that feed two fully connected layers, with the SGD optimizer, and the MSE or Cross Entropy loss function.
  This is just a learning experiment (learning Pytorch for ~3 months now). I am interested in examining the kernels when a sufficiently low loss and high accuracy is reached.  I am training the network on 100 100x100 grayscale images that contain round or square pulses like these
  
  or
  
  The output is a 3 neuron layer that can be 000=round, 001=square, etc.  In all data pairs, 1 is mapped to 0.8 and 0 to 0.1.
  As training proceeds, I was hoping to watch the kernels evolve from random to forms containing corners, lines and slopes; in other words, general features of my data.  Here's what I see:
  If I choose a learning rate that gives finite, numerical loss values (lr=0.01, etc.) the kernels look like this
  
  and do not change at all over 1000s of epochs, even though the loss can get arbitrarily low (1e-10 if I want), with 90%+ accuracy is classifying the pulses.
  If I choose a large learning rate, like (lr=2 or lr=5), I get nan values for the learning rate, but neat looking kernels like this:
  
  but also do not change at all over 1000s of epochs.
  Am I wrong to expect the kernels to evolve from random to containing features of the training set as training proceeds?  I see no evolution of the kernels at all.
  Thanks for any input. I am hoping features seen in kernels of trained networks might help those in the basic sciences (physics, chem, etc.) with new perspectives on their data.
  Edit a day later:
  I kept working on it, and normalizing the output (using Torch's normalize) seems to help. Watch this:
  
  Edit on 12-Aug-2024, in response to @NikoNyrh's comment:
  Dear @NikoNyrh: Thanks for your input and for sharing your fine results. I've kept at it on my end, and was able to find some similar things as you. In particular:
  
  I agree my input figures are simple (maybe too simple). If I use 30+ kernels (as per a typical MNIST model), the kernels stay random looking, even though the loss lowers and converges. As you indicated, this number of kernels with the relatively simple inputs may be too many parameters, making a loss minimum easy for the network to find, without revealing any structure in the kernels.
  
  I decided to restrict the network some, cutting the kernels down to 5 (20x20 kernels on 500x500 images), to force more a more selective minimum.  For more diversity in the images, I allow the waveforms to appear at any position in the image (even if it gets cut off by the image boundary).
  
  I reworked my training plan. I got access to a GPU and trained on 10,000 images and look for accuracy on 100 images not in in the training set. I get about 17s/epoch on a 4090 consuming 71 Watts.
  
  I too achieve 99%+ accuracy with the 100 test images.
  
  I found the network very finicky to train, but this seems to work: L1Loss, Tanh activation on the fully connected layers, lr=0.1, momentum=0.0, batch_size=1,000 (on the 10,000 input images), and very selective DropOut.
  
  My network is huge (200M parameters) as shown here:
  
  
      class neural_net(nn.Module):
          def __init__(self):
              super(neural_net, self).__init__()
              self.conv_layer_count = 5
              self.K = 25
      
              self.conv1 = nn.Conv2d(in_channels=1, out_channels=self.conv_layer_count, kernel_size=self.K, stride=1, padding=0)
              self.relu = nn.ReLU()
              self.mp1 =  nn.MaxPool2d(2)
              self.conv2 = nn.Conv2d(in_channels=self.conv_layer_count, out_channels=1, kernel_size=self.K)
              self.act = nn.Tanh()
              self.dropout1 = nn.Dropout(0.01)
              self.fc1 = nn.Linear(226*226, 4096)
              self.fc2 = nn.Linear(4096, 1024)
              self.fc3 = nn.Linear(1024, 3)
      
      
          def forward(self, x):
              x = self.conv1(x)
              x = self.relu(x)
      
              x = self.conv2(x)
              x = self.relu(x)
              x = self.mp1(x)
      
        
              x = self.dropout1(x)
      
              x = torch.flatten(x, 1)
              x = self.fc1(x)
              x = self.relu(x)
      
              x = self.fc2(x)
              x = self.act(x)
      
              x = self.fc3(x)
              x = self.act(x)
      
              return x
  
  
  The Loss profile is interesting, with all kinds of ups and downs on the way to convergence.
  
  
  
  Here are some of my eventual kernels that like yours, show the vertical and horizontal lines, including some 45 degree slopes, and wisps of other curves as well.
  
  
  
  Thanks again @NikoNyrh!! Very interesting results.
  
  **Tags:** convolutional-neural-networks, filters
  > **Comments:**
  > **username_1**: Neat GIF.  You mentioned before the Edit that you saw "no evolution of the kernels at all."  Can you say more about how you normalized the output to achieve the evolution visible in the GIF?  
  > **username_0**: @SapereAude: In my forward() function, I literally added `x=F.normalize(x)`, where x is the vector coming off of the activation of the last and fully connected layer in the network.  `x` out of the normalization is then returned by forward().  
  
  ---
  
  ## 1 answers
  
  ### Answer by username_2 on 2024-08-10. Score: 0
  
  This question turned out to be more interesting than I thought. Basically I think that the problem is too simple for CNNs, and any random-looking feature extraction gets it done.
  To get the model to converge to a structure I was expecting, I had to make the network very simple, and use a specific initialization. First of all, here is the Keras code:
  model = tf_models.Sequential([
      l.InputLayer(X.shape[1:]),
      l.Conv2D(7, 9, activation='relu', kernel_initializer=keras.initializers.Constant(init)),
      l.BatchNormalization(),
      l.GlobalMaxPooling2D(),
      l.Dense(1, activation='sigmoid', kernel_initializer='zeros', use_bias=False)
  ])
  
  The generated data looks like this (vertical slopes very tricky, because I used the formula $f(x,y) \approx 0$), each input is a 128 x 128 single-channel image:
  
  The last layer's weights were initialized to zero, here is how they (along with loss and val_loss) changed trough-out the training procedure:
  
  So some Conv2D kernels learned to indicate a gaussian curve, and others indicate the pulse.
  Here is the interesting part: Conv2D weights are initialized to "linear" gradients, to seven different directions. They are shown on the first column of this image. Subsequent columns show how the weights changed trough-out the training procedure. Final weights are shown on y-axis labels. Negative weights detect gaussians and positive ones detect pulses.
  
  The pattern becomes a bit more clear if we sort the kernels by their corresponding dense layer's weights:
  
  So gradients on roughly 45 degree angles didn't change much (they are already good detectors for pulse's specific parts), while vertical gradients become "hard" vertical edge detectors.
  If instead I use random initialization for Conv2D, I don't get so clear patterns or conclusions. On one instance I got five detectors of vertical lines, and two almost identical detectors for -45 degree slopes:
  
  For what it is worth, I got a model with only seven parameters to learn greater than 99.9% train and validation accuracy.
  
  
  ---
  
  