6

In one of my previous questions, the magnificent Jake helped me generate a heatmap that looks identical to using imagesc on a 2D matrix within MATLAB.

The results look great, but now I'm looking to modify its behaviour in order to create a Hinton diagram. A Hinton diagram is a cool way of visualising the magnitudes and directions of the weights in a neural network. Instead of using a colourmap to decide the colour of each "pixel" (e.g., black is -1, white is 1 and anything in between is a scale of grey), it uses colours x and y for negative and positive values respectively, and the magnitude is visualised by the size of the square marker. Below is an example output from a Python interpretation (the code might inspire someone).

enter image description here

Ideally, it would be possible to tuck all this away in a style.

As a bonus, it'd be great to have the ability to read a file that stores the data in a matrix's natural representation:

 2.76658    -0.07990     0.10551    -2.94131     0.00840     0.23832    -2.56759     0.04593
-0.21476     2.37350    -2.30670     0.11634    -2.36124     2.62034    -0.32261     2.36860
 0.01118    -2.42926     2.42470     0.08698     2.45065    -2.39544    -0.16931    -2.32933
-3.04568    -0.15376     0.03051     2.74830    -0.07237     0.02359     2.96758     0.05812
-0.12791    -2.50370     2.63524     0.15000     2.26310    -2.39198    -0.05032    -2.41050
-0.07663     2.40350    -2.45346    -0.00479    -2.62160     2.29896    -0.11746     2.49031
-2.90385    -0.11742    -0.15037     2.88956     0.01517    -0.06700     2.96463    -0.10442
-0.17761     2.01661    -2.23660    -0.07113    -2.39688     2.19306     0.07902     2.37361

Or, if that's not feasible, in a traditional x-y-data format like this:

0 0 2.766580
1 0 -0.079900
2 0 0.105510
3 0 -2.941310
4 0 0.008400
5 0 0.238320
6 0 -2.567590
7 0 0.045930
0 1 -0.214760
1 1 2.373500
2 1 -2.306700
3 1 0.116340
4 1 -2.361240
5 1 2.620340
6 1 -0.322610
7 1 2.368600
0 2 0.011180
1 2 -2.429260
2 2 2.424700
3 2 0.086980
4 2 2.450650
5 2 -2.395440
6 2 -0.169310
7 2 -2.329330
0 3 -3.045680
1 3 -0.153760
2 3 0.030510
3 3 2.748300
4 3 -0.072370
5 3 0.023590
6 3 2.967580
7 3 0.058120
0 4 -0.127910
1 4 -2.503700
2 4 2.635240
3 4 0.150000
4 4 2.263100
5 4 -2.391980
6 4 -0.050320
7 4 -2.410500
0 5 -0.076630
1 5 2.403500
2 5 -2.453460
3 5 -0.004790
4 5 -2.621600
5 5 2.298960
6 5 -0.117460
7 5 2.490310
0 6 -2.903850
1 6 -0.117420
2 6 -0.150370
3 6 2.889560
4 6 0.015170
5 6 -0.067000
6 6 2.964630
7 6 -0.104420
0 7 -0.177610
1 7 2.016610
2 7 -2.236600
3 7 -0.071130
4 7 -2.396880
5 7 2.193060
6 7 0.079020
7 7 2.373610

You can generate the second data format from the first using a Python script like this:

import numpy as np
rawdata = np.loadtxt('data.dat')
coords = np.meshgrid(np.arange(np.shape(rawdata)[0]),
                     np.arange(np.shape(rawdata)[1]))
data = np.vstack([coords[0].flatten(), coords[1].flatten(), rawdata.flatten()]).T
np.savetxt('weights-example.dat', data, fmt="%i %i %f")

This is probably a bit too much to ask of the TikZ/pgfplots gurus as I have not really put any effort into solving this, but, alas, I cannot even fully understand how Jake's code works. If anyone's willing to help, I guess Jake's answer to my previous question would be the place to start.

  • I know nothing. But two questions come to my mind: (1) should the biggest square possible be the one with the biggest absolute value? In that case it wouldn't be easier if the numbers provided where between -1 and 1?; (2) does the magnitude represent the area of the square or the length of the sides? I suppose the answer is the first one. – Manuel Jan 21 '14 at 21:37
  • @Manuel (1) Yes, the biggest square should be the one with the biggest absolute value. So all squares have the same area, but only the maximum magnitude uses the whole area. It would help if the data were normalised in the range [-1 1], but I'd imagine that pgfplots provides the capability to normalise the data on the go. (2) I think it represents the area of the square. I could be wrong though! That' why I linked to the Python code! :-) – sudosensei Jan 21 '14 at 21:53
  • You can use the pgf backend of matplotlib and export the diagram to pgf code, which you can then \input into your latex document. Would that be acceptable? – Psirus Jan 21 '14 at 22:38
  • @Psirus I would be interested in that answer and I would definitely up-vote it (as would many others I believe), but I'd prefer a solution that's not as involved. – sudosensei Jan 21 '14 at 22:45
9

Here's a way to do this in PGFPlots. I used the x,y,value data instead of the matrix, because that's much easier to process using PGFPlots.

I've defined a fenton style which can be added to the axis options. With your test file and the following code...

\begin{axis}[fenton]
\addplot table [meta index=2] {weights-example.dat};
\end{axis}

... you'll get

The styles for the positive and negative values can be set using fenton/positive style={...} and fenton/negative style={...}.


Here's the full code:

\documentclass[border=5mm]{standalone}
\usepackage{pgfplots}
\pgfplotsset{compat=1.9}

\makeatletter
\pgfplotsset{
    fenton/positive/.style={fill=white, draw=none},
    fenton/positive style/.style={/pgfplots/fenton/positive/.append style={#1}},
    fenton/negative/.style={fill=black, draw=none},
    fenton/negative style/.style={/pgfplots/fenton/negative/.append style={#1}},
    fenton/.style={
        axis background/.style={fill=gray},
        only marks, no markers,
        scatter,
        axis equal image,
        axis on top,
        major tick length=0pt,
        enlarge x limits={abs=0.5},
        enlarge y limits={abs=0.5},
        point meta=explicit,
        scatter/@pre marker code/.code={
            \pgfkeys{/pgf/fpu,/pgf/fpu/output format=fixed}
            \pgfmathsetmacro\normalisedsize{
                sqrt(
                    abs(
                        \pgfplotspointmeta / max(
                            abs(\pgfplots@metamin),
                            abs(\pgfplots@metamax)
                        )
                    )
                )
            }
            \pgfmathfloatifflags{\pgfplotspointmeta}{+}{
                \pgfplotsset{fenton/current value/.style=/pgfplots/fenton/positive}
            }{
                \pgfplotsset{fenton/current value/.style=/pgfplots/fenton/negative}
            }
            \path [/pgfplots/fenton/current value] ([scale=\normalisedsize]axis direction cs:-0.5,-0.5) rectangle ([scale=\normalisedsize]axis direction cs:0.5,0.5);   
        },
        scatter/@post marker code/.code={}
    }
}
\makeatother

\begin{document}  

\begin{tikzpicture}
\begin{axis}[fenton]
\addplot table [meta index=2] {weights-example.dat};
\end{axis}
\end{tikzpicture}
\end{document}
|improve this answer|||||
  • Fantastic work like usual, Jake! Thanks a ton! You make me love TikZ and pgfplots more by the day. – sudosensei Jan 22 '14 at 18:07
  • @sudosensei: Thanks for the compliments! =) Glad it helps – Jake Jan 22 '14 at 21:21
10

I think this does it. Could have done it in pure tex but today I am lazy, and so this requires lualatex. The file reading method is not robust. Also I've used just black or white colors (no in between shading).

\documentclass[tikz,border=.1cm]{standalone}
\usepackage{filecontents}

\begin{filecontents*}{data.dat}
2.76658    -0.07990     0.10551    -2.94131     0.00840     0.23832    -2.56759     0.04593
-0.21476     2.37350    -2.30670     0.11634    -2.36124     2.62034    -0.32261     2.36860
 0.01118    -2.42926     2.42470     0.08698     2.45065    -2.39544    -0.16931    -2.32933
-3.04568    -0.15376     0.03051     2.74830    -0.07237     0.02359     2.96758     0.05812
-0.12791    -2.50370     2.63524     0.15000     2.26310    -2.39198    -0.05032    -2.41050
-0.07663     2.40350    -2.45346    -0.00479    -2.62160     2.29896    -0.11746     2.49031
-2.90385    -0.11742    -0.15037     2.88956     0.01517    -0.06700     2.96463    -0.10442
-0.17761     2.01661    -2.23660    -0.07113    -2.39688     2.19306     0.07902     2.37361
\end{filecontents*}

{\catcode`\%=11\gdef\pc{%}}
\begin{document}
\directlua{%
Matrix = {}
Matrix.__index = Matrix

function Matrix.new()
  local obj = {nrow=0, ncol=0, max=-math.huge, min=math.huge, data={}}
  setmetatable(obj, Matrix)
  return obj
end
function Matrix.fromFile(f)
  local obj, file, line, row, v, c
  obj = Matrix.new()
  file = io.open(f)
  if file then
     for line in file:lines() do
       row = {}
       c = 0
       for v in string.gmatch(line, "\pc S+") do
          v = tonumber(v)
          if v > obj.max then obj.max = v end
          if v < obj.min then obj.min = v end
          table.insert(row, v)
          c = c + 1
       end
       table.insert(obj.data, row)
       obj.nrow = obj.nrow + 1
       if c > obj.ncol then
          obj.ncol = obj.ncol + 1
       end
     end
  end
  return obj
end

function Matrix:get(i, j)
  if self.data[i][j] then
    return self.data[i][j]
  else
    return 0
  end
end  

function Matrix:set(i, j, v)
  if self.data[i] == nil then
    table.insert(self.data, i, {})
  end
  table.insert(self.data[i], j, v)
  if i > self.nrow then self.nrow = i end
  if j > self.ncol then self.ncol = j end
end

function Matrix:normalize()
  local i, j, v
  for i = 1,self.ncol do
    for j = 1,self.nrow do
      self.data[i][j] = 2*(self.data[i][j] - self.min) / (self.max - self.min) - 1
    end
  end
  self.max = 1
  self.min = -1
end
}

\def\getlua#1{\directlua{tex.print("" .. #1)}}

\directlua{
  M = Matrix.fromFile("data.dat")
  M:normalize()
}
\newcommand\hinton[2][]{%
\begin{tikzpicture}[#1]
\fill [black!15] (0,0) rectangle ++(\getlua{#2.ncol}+1, \getlua{#2.nrow}+1);
\foreach \i in {1,...,\getlua{#2.ncol}}{
  \foreach \j [evaluate={\v=\getlua{#2:get(\i, \j)}; \c=(\v<0 ? 100 : 0); \s=\v; }] in {1,...,\getlua{#2.nrow}}{
    \fill [black!\c!white] (\i, \j) ++(-\s/2,-\s/2) rectangle ++(\s,\s);
  }}
\end{tikzpicture}}

\hinton{M}

\directlua{%
N = Matrix.new()
local i, j
for i = 1,25 do
  for j = 1,25 do
    N:set(i, j, math.random()*2-1)
  end
end
}

\hinton[x=5pt, y=5pt]{N}
\end{document}

enter image description here

enter image description here

|improve this answer|||||
  • Thanks a lot! This is a lovely answer. But since the original question was about doing this with pgfplots (personal preference), I have to accept Jake's answer. I appreciate your help though. – sudosensei Jan 22 '14 at 18:05
9

As mentioned in the comments, you can export the matplotlib figure to pgf code and in turn compile this with latex. I added a small bit of code to generate some fake data, and replaced the P.show() with P.savefig('hinton_code.pgf') to write the diagram to the file:

import numpy as N
import pylab as P

def _blob(x,y,area,colour):
    hs = N.sqrt(area) / 2
    xcorners = N.array([x - hs, x + hs, x + hs, x - hs])
    ycorners = N.array([y - hs, y - hs, y + hs, y + hs])
    P.fill(xcorners, ycorners, colour, edgecolor=colour)

def hinton(W, maxWeight=None):
    reenable = False
    if P.isinteractive():
        P.ioff()
    P.clf()
    height, width = W.shape
    if not maxWeight:
        maxWeight = 2**N.ceil(N.log(N.max(N.abs(W)))/N.log(2))
    P.fill(N.array([0,width,width,0]),N.array([0,0,height,height]), 'gray')
    P.axis('off')
    P.axis('equal')
    for x in xrange(width):
        for y in xrange(height):
            _x = x+1
            _y = y+1
            w = W[y,x]
            if w > 0:
                _blob(_x - 0.5, height - _y + 0.5, min(1,w/maxWeight),'white')
            elif w < 0:
                _blob(_x - 0.5, height - _y + 0.5, min(1,-w/maxWeight), 'k')
    if reenable:
        P.ion()
    P.savefig('hinton_code.pgf')

if __name__ == "__main__":
    W = N.random.rand(15, 15) - 0.5*N.ones((15,15))
    hinton(W)

Creating a document would then be as simple as

\documentclass{article}
\usepackage{pgf}
\begin{document}
\input{hinton_code.pgf}
\end{document}

Also, I expect reading in the data file to be much easier in Python than it would be in TeX.

|improve this answer|||||
  • Thanks! This answer does exactly what was asked, except the pgfplots part. Since there's a pgfplots solution, I can't mark this as the accepted answer though. – sudosensei Jan 22 '14 at 17:57
5

Here is a very simple solution using only TikZ:

\documentclass[tikz]{standalone}
\usetikzlibrary{fit,backgrounds}
\def\mymatrix{%
  {2.76658, -0.07990, 0.10551, -2.94131, 0.00840, 0.23832, -2.56759, 0.04593},
  {-0.21476, 2.37350, -2.30670, 0.11634, -2.36124, 2.62034, -0.32261, 2.36860},
  {0.01118, -2.42926, 2.42470, 0.08698, 2.45065, -2.39544, -0.16931, -2.32933},
  {-3.04568, -0.15376, 0.03051, 2.74830, -0.07237, 0.02359, 2.96758, 0.05812},
  {-0.12791, -2.50370, 2.63524, 0.15000, 2.26310, -2.39198, -0.05032, -2.41050},
  {-0.07663, 2.40350, -2.45346, -0.00479, -2.62160, 2.29896, -0.11746, 2.49031},
  {-2.90385, -0.11742, -0.15037, 2.88956, 0.01517, -0.06700, 2.96463, -0.10442},
  {-0.17761, 2.01661, -2.23660, -0.07113, -2.39688, 2.19306, 0.07902, 2.37361}%
}
\def\max{3}
\begin{document}
\begin{tikzpicture}
   \foreach \line [count=\y] in \mymatrix {
     \foreach \val [count=\x] in \line {
       \pgfmathsetmacro\col{(\val>=0)?"orange":"lime"}
       \pgfmathsetmacro\len{sqrt(abs(\val)/\max)/2}
       \fill[fill=\col] (\x,-\y) +(\len cm,\len cm) rectangle +(-\len cm,-\len cm);
     }
   }
   \begin{scope}[on background layer]
     \node[fill=gray,fit=(current bounding box),inner sep=1mm,draw]{};
   \end{scope}
\end{tikzpicture}
\end{document}

enter image description here

|improve this answer|||||
  • +1 It is very easy to implement this algorithm with PSTricks. – kiss my armpit Feb 7 '14 at 0:08
  • +1! Nice one. I didn't think that a TikZ-only solution would be this simple and elegant. – sudosensei Feb 7 '14 at 21:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.