(A FAIRE : trouver un titre)

Problématique
Plan de ce document
Données
Idée 1 : Réduction de la dimension
Idée 2
Idée 2 bis
Idée 3 : Problème du voyageur de commerce
A FAIRE

Attention : ce document est encore à l'état de brouillon

A FAIRE : Remettre de l'rodre dans ce document
0. Problématique, données
1. Noyaux
2. Réseaux de neurones
3. k-means + MST
4. Isomap
5. Optimisation
6. pruned MST
7. TSP
8. SVM

Problématique

On dispose d'un nuage de points (40), dans un espace de dimension raisonnable (10) ou moins raisonnable (7000). Ces points sont à peu près alignés sur une courbe que l'on cherche à décrire.

L'exemple motivant ce problème est le suivant. A l'aide de puces à ADN, on a examiné l'expression de 7000 gènes chez 40 patients souffrant d'une même maladie, mais à des stades différents. On s'en apperçoit en effectuant une analyse en composantes principales (ou en appliquant d'autres méthodes de réduction de la dimension) : les points correspondant aux patients semblent alignés le long d'une courbe (en dimension deux, cette courbe peut présenter des singularités, par exemple, elle peut se recouper -- mais si on tient compte des dimensions suivantes, ce problème disparait). A l'aide de l'expression de ces gènes (ou juste de certains de ces gènes) on aimerait pouvoir définir un "indice" qui indique la position sur la courbe et donc le degré d'avancement de la maladie.

Plan de ce document

Après avoir présenté les quelques jeux de données (réels ou simulés) sur lesquels nous travaillerons, nous commencerons par appliquer des méthodes de réduction de la dimension (analyse en composantes principales après des transformations non linéaires, réseaux de neurones), des méthodes d'optimisation et des méthodes de résolution du problème du voyageur de commerce, puis nous comparerons toutes ces méthodes.

Dans un dernier temps, nous considérerons un problème un peu différent : on dispose du nuage de points et, pour chaque point, son indice. On tentera de prédire l'indice d'un nouveau point.

Données

Données simulées : ligne brisée

Au lieu d'une courbe, commençons par prendre une ligne brisée.

n <- 200  # Nombre de sujets, nombre de colonnes
k <- 10   # Dimension de l'espace dans lequel la courbe recherchée vit
nb.points <- 5
p <- matrix( 5*rnorm(nb.points*k), nr=k )
barycentre <- function (x, n) {
  # Ajouter des nombres entre les valeurs de x afin d'obtenir un
  # vecteur de longueur n. 
  i <- seq(1,length(x)-.001,length=n)
  j <- floor(i)
  l <- i-j
  (1-l)*x[j] + l*x[j+1]
}
m <- apply(p, 1, barycentre, n)
data.broken.line <- t(m)
pairs(t(data.broken.line))

*

plot(princomp(t(data.broken.line)))

*

pairs(princomp(t(data.broken.line))$scores[,1:5])

*

Données simulées : ligne brisée bruitée

data.noisy.broken.line <- data.broken.line + rnorm(length(data.broken.line))
pairs(princomp(t(data.noisy.broken.line))$scores[,1:5])

*

Données simulées : courbe

Pour avoir une courbe au lieu des segments, il suffit de changer notre fonction "barycentre".

library(splines)
barycentre2 <- function (y,n) {
  m <- length(y)
  x <- 1:m
  r <- interpSpline(x,y)    
  #r <- lm( y ~ bs(x, knots=m) )
  predict(r, seq(1,m,length=n))$y
}

k <- 5
y <- sample(1:100,k)
x <- seq(1,k,length=100)
plot(barycentre2(y,100) ~ x)
lines(y, col='red', lwd=3, lty=2)

*

data.curve <- apply(p, 1, barycentre2, n)
data.curve <- t(data.curve)
pairs(t(data.curve))

*

plot(princomp(t(data.curve)))

*

pairs(princomp(t(data.curve))$scores[,1:5])

*

On va quand-même ajouter un peu de bruit :

data.noisy.curve <- data.curve + rnorm(length(data.curve))
pairs(t(data.noisy.curve))

*

plot(princomp(t(data.noisy.curve)))

*

pairs(princomp(t(data.noisy.curve))$scores[,1:5])

*

Données simulées plus complexes

Réfléchissons un peu sur la forme possible de la courbe que l'on va obtenir. S'il s'agit d'une maladie mortelle (ou si, chez tous les patients de l'étude, la maladie évolue vers la mort), on peut s'attendre à observer un segment, éventuellement un peu déformé, une extrémité correspondant aux individus sains, l'autre à la mort.

S'il s'agit d'une maladie bénigne, on pourrait s'attendre à observer des points répartis le long d'un cercle, correspondant au cycle sain, malade, en rémission, sain.

S'il s'agit d'une maladie parfois mortelle, mais parfois curable, on peut imaginer un mélange des deux : une réunion d'un cercle et d'un segment.

random.rotation.matrix <- function (n=3) {
  m <- NULL
  for (i in 1:n) {
    x <- rnorm(n)
    x <- x / sqrt(sum(x*x))
    y <- rep(0,n)
    if (i>1) for (j in 1:(i-1)) {
      y <- y + sum( x * m[,j] ) * m[,j]
    }
    x <- x - y
    x <- x / sqrt(sum(x*x))
    m <- cbind(m, x)
  }
  m
}

n <- 200
k <- 10
x <- seq(0,2*pi,length=n)
data.circle <- matrix(0, nr=n, nc=k)
data.circle[,1] <- cos(x)    
data.circle[,2] <- sin(x)
data.circle <- data.circle %*% random.rotation.matrix(k)
data.circle <- t( t(data.circle) + rnorm(k) )
pairs(data.circle[,1:3])

*

data.circle <- data.circle + .1*rnorm(n*k)
pairs(data.circle[,1:3])

*

pairs(princomp(data.circle)$scores[,1:3])

*

A FAIRE : l'autre exemple.

Données réelles

data.real <- read.table("Tla_z.txt", sep=",")
data.real.group <- factor(substr(names(data.real),0,1))
r <- prcomp(t(data.real))
plot(r$sdev, type='h')

*

data.real.3d <- r$x[,1:3]
pairs(data.real.3d, pch=16, col=as.numeric(data.real.group))

*

Pour voir un peu mieux ce qui se passe, on peut tenter de mettre les points d'un même groupe dans une ellipse.

draw.ellipse <- function (
    x, y=NULL, N=100,
    method=lines, ...
  ) {
  if (is.null(y)) {
    y <- x[,2]
    x <- x[,1]
  }
  centre <- c(mean(x), mean(y))
  m <- matrix(c(var(x),cov(x,y),
                cov(x,y),var(y)),
              nr=2,nc=2)
  e <- eigen(m)
  r <- sqrt(e$values)
  v <- e$vectors
  theta <- seq(0,2*pi, length=N)
  x <- centre[1] + r[1]*v[1,1]*cos(theta) +
       r[2]*v[1,2]*sin(theta)
  y <- centre[2] + r[1]*v[2,1]*cos(theta) +
       r[2]*v[2,2]*sin(theta)
  method(x,y,...)
}
draw.star <- function (x, y=NULL, ...) {
  if (is.null(y)) {
    y <- x[,2]
    x <- x[,1]
  }
  d <- cbind(x,y)
  m <- apply(d, 2, mean)
  segments(m[1],m[2],x,y,...)
}
my.plot <- function (
    d, f=rep(1,dim(d)[1]),
    col=rainbow(length(levels(f))),
    variables=NULL, legend=T, legend.position=1,
    draw=draw.ellipse, ...) {
  xlim <- range(d[,1])
  ylim <- range(d[,2])
  if(!is.null(variables)){
    xlim <- range(xlim, variables[,1])
    ylim <- range(ylim, variables[,2])
  }
  plot(d, col=col[as.numeric(f)], pch=16,
       xlim=xlim, ylim=ylim, ...)
  for (i in 1:length(levels(f))) {
    try(
      draw(d[ as.numeric(f)==i, ], col=col[i])
    )
  }
  if(!is.null(variables)){
    arrows(0,0,variables[,1],variables[,2])
    text(1.05*variables,rownames(variables))
  }
  abline(h=0,lty=3)
  abline(v=0,lty=3)
  if(legend) {
    if(legend.position==1) {
      l=c( par('usr')[1],par('usr')[4], 0, 1 )
    } else if (legend.position==2) {
      l=c( par('usr')[2],par('usr')[4], 1, 1 )
    } else if (legend.position==3) {
      l=c( par('usr')[1],par('usr')[3], 0, 0 )
    } else if (legend.position==4) {
      l=c( par('usr')[2],par('usr')[3], 1, 0 )
    } else {
      l=c( mean(par('usr')[1:2]),
           mean(par('usr')[1:2]), .5, .5 )
    }
    legend(l[1], l[2], xjust=l[3], yjust=l[4],
           levels(f),
           col=col,
           lty=1,lwd=3)
  }
}
my.plot(data.real.3d[,1:2], data.real.group)

*

op <- par(mfrow=c(3,3), mar=c(0,0,0,0))
plot.new();
my.plot(data.real.3d[,c(2,1)], data.real.group, 
        xlab='',ylab='',axes=F,legend=F)
box()
my.plot(data.real.3d[,c(3,1)], data.real.group, 
        xlab='',ylab='',axes=F,legend=F)
box()
  
my.plot(data.real.3d[,c(1,2)], data.real.group, 
        draw=draw.star, 
        xlab='',ylab='',axes=F,legend=F)
box()
plot.new()
my.plot(data.real.3d[,c(3,2)], data.real.group, 
        xlab='',ylab='',axes=F,legend=F)
box()

my.plot(data.real.3d[,c(1,2)], data.real.group, 
        draw=draw.star, 
        xlab='',ylab='',axes=F,legend=F)
box()
my.plot(data.real.3d[,c(2,3)], data.real.group, 
        draw=draw.star, 
        xlab='',ylab='',axes=F,legend=F)
box()
plot.new()
par(op)

*

On peut aussi faire tourner l'image dans tous les sens.

set.seed(66327)
random.rotation.matrix <- function (n=3) {
  m <- NULL
  for (i in 1:n) {
    x <- rnorm(n)
    x <- x / sqrt(sum(x*x))
    y <- rep(0,n)
    if (i>1) for (j in 1:(i-1)) {
      y <- y + sum( x * m[,j] ) * m[,j]
    }
    x <- x - y
    x <- x / sqrt(sum(x*x))
    m <- cbind(m, x)
  }
  m
}
op <- par(mfrow=c(3,3), mar=c(0,0,0,0))
for (i in 1:9) {
  #plot( (data.real.3d %*% random.rotation.matrix(3))[,1:2],
  #      pch=16, col=as.numeric(data.real.group),
  #      xlab='', ylab='', axes=F )
  my.plot((data.real.3d %*% random.rotation.matrix(3))[,1:2],
          data.real.group, 
          draw=draw.ellipse, 
          xlab='',ylab='',axes=F,legend=F)
  box()
}
par(op)

*

Idée 1 : Réduction de la dimension

Analyse en composantes principales et noyaux

Des méthodes comme l'analyse en composantes principales (PCA) permettent de réduire la dimension du nuage de points mais, étant linéaires, ne permettent pas de "déméler" la courbe observée : elle restera courbe et ne donnera pas un segment.

Une idée consiste à plonger notre espace (de dimension raisonnable (10) ou non (7000)) dans un espace de dimension plus grande, de manière non linéaire. En appliquant des méthodes linéaires dans cet espace plus grand, on peut espérer mettre en évidence des phénomènes non linéaires dans l'espace de départ.

Voici un exemple de tel plongement :

(x,y)  --->   (x^2, x*y, y^2).

Il peut paraître déraisonnable de plonger un espace de dimension déjà élevée dans un espace de dimension encore plus grande. Ce serait le cas si on avait besoin de calculer les coordonnées des points dans ce nouvel espace, mais nous avons juste besoin de savoir calculer des produits scalaires dans ce nouvel espace.

Plus précisément, l'analyse en composantes principales (ou d'autres méthodes semblables) partent de la matrice de variance-covariance

t(m) %*% m

Avec la matrice de variance-covariance dans le nouvel espace, ça donne :

m <- data.curve
mm <- apply(m, 2, function (x) { x %o% x } )
r <- princomp(t(mm))
plot(r)

*

pairs(r$scores[,1:5])

*

Rien de très concluant...

Essayons en degré 3.

# Noyau de degré 1
noyau1 <- function (x,y) { sum(x*y) }
m <- data.curve
m <- t(t(m) - apply(m,2,mean))
k <- dim(m)[1]
wrapper <- function(x, y, my.fun, ...) {
  sapply(seq(along=x), FUN = function(i) my.fun(x[i], y[i], ...))
}
mm <- outer(1:k, 1:k, wrapper, function (i,j) { noyau1(m[,i],m[,j]) })

# Noyau de degré 2
noyau2 <- function (x,y) {
  a <- x*y
  n <- length(a)
  i <- gl(n,1,n^2)
  j <- gl(n,n,n^2)
  i <- as.numeric(i)
  j <- as.numeric(j)
  w <- which( i <= j & j <= k )
  i <- i[w]
  j <- j[w]
  sum(a[i]*a[j])
}
stopifnot( noyau2(1:2,2:1) == 12 )

# Noyau de degré 3
noyau3 <- function (x,y) {
  a <- x*y
  n <- length(a)
  i <- gl(n,1,n^3)
  j <- gl(n,n,n^3)
  k <- gl(n,n^2,n^3)
  i <- as.numeric(i)
  j <- as.numeric(j)
  k <- as.numeric(k)
  w <- which( i <= j & j <= k )
  i <- i[w]
  j <- j[w]
  k <- k[w]
  sum(a[i]*a[j]*a[k])
}
stopifnot( noyau3(1:2,2:1) == 32 )
  
wrapper <- function(x, y, my.fun, ...) {
  sapply(seq(along=x), FUN = function(i) my.fun(x[i], y[i], ...))
}
k <- dim(m)[1]
mm <- outer(1:k, 1:k, FUN=wrapper, my.fun = function (i,j) { noyau3(m[,i],m[,j]) })

r <- princomp(covmat=mm)
plot(r)

*

#pairs(r$scores[,1:5])
plot((t(m) %*% r$loadings) [,1:2])

*

k <- dim(data.noisy.curve)[1]
mm <- outer(1:k, 1:k, FUN=wrapper, my.fun = function (i,j) { 
  noyau3(data.noisy.curve[,i],data.noisy.curve[,j]) 
})
r <- princomp(t(data.noisy.curve), covmat=mm)
plot(r)

*

pairs((t(m) %*% r$loadings) [,1:5])

*

plot((t(m) %*% r$loadings) [,1:2])
lines((t(m) %*% r$loadings) [,1:2], col='red')

*

x <- 1:dim(data.noisy.curve)[2]
y1 <- (t(m) %*% r$loadings) [,1]
y2 <- (t(m) %*% r$loadings) [,2]
r1 <- loess(y1~x)
r2 <- loess(y2~x)
plot(y1,y2)
lines(y1,y2, col='red')
lines(predict(r1), predict(r2), col='blue', lwd=3)

*

Essayons en combinant ces différents noyaux.

noyau <- function (x,y) {
  noyau1(x,y) + noyau2(x,y) + noyau3(x,y)
}
mm <- outer(1:k, 1:k, FUN=wrapper, my.fun = function (i,j) { 
  noyau(data.curve[,i],data.curve[,j]) 
})
r <- princomp(covmat=mm)
plot(r)

*

pairs((t(m) %*% r$loadings) [,1:5])

*

En définitive, ces méthodes sont peu concluantes.

Réduction de la dimension à l'aide de réseaux de neurones

On peut réduire la dimension d'un nuage de points à l'aide d'un réseau à 5 couches, une couche d'entrée (avec les données), une couche de compression (non linéaire), une couche représentant les données dans l'espace de dimension réduite recherché (linéaire), une couche de décompression (non linéaire), une couche de sortie (sur laquelle on essaye de retrouver les données de départ).

Je n'ai pas l'impression que R prévoie de tels réseaux, on va donc les implémenter nous-même. (Il faudra faire TRÈS attention à la convergence.)

Comme je ne connais pas vraiment les réseaux de neurones (je m'y suis intéressé très superficiellement il y a une dizaine d'années), je cherche un peu sur internet et je trouve une page qui les explique assez clairement et, en particulier, qui mentionne l'application à la réduction de la dimension :

http://www.willamette.edu/%7Egorr/classes/cs449/backprop.html

Voici le code que j'en déduis.

A FAIRE : commencer par regarder sur un exemple de petite taille,
  pour lequel le calcul sera rapide, et pour lequel on saiit à quel
  résultat s'attendre. Par exemple, prendre un nuage des points de
  dimension 2 dans un espace de dimension 3, avec uniquement des
  neurones linéaires et juste deux neurones dans la couche du
  milieu.
  Problème : dans ce cas, les couches intermédiaires ne servent pas
  à grand-chose, et risquent même de nuire à la stabilité de
  l'algorithme.

# x : matrice, les colonnes sont les différents vecteurs à apprendre
# n : nombre de neurones sur la couche du milieu
# m : nombre de neurones sur les deux autres couches cachées
#     (on devrait avoir m>n)
drnn <- function (M, n, m, e=.1, N=100) {
  # Les fonctions d'activation
  id <- function (t) { t }
  f1 <- tanh
  f2 <- id
  f3 <- tanh
  f4 <- id
  # Le nombre de neurones dans chaque couche
  n0 <- dim(M)[1]
  n1 <- m
  n2 <- n
  n3 <- m
  n4 <- n0
  # Les poids -- on les initialise à des valeurs aléatoires
  w1 <- matrix( rnorm(n0*n1), nc=n0, nr=n1 )
  w2 <- matrix( rnorm(n1*n2), nc=n1, nr=n2 )
  w3 <- matrix( rnorm(n2*n3), nc=n2, nr=n3 )
  w4 <- matrix( rnorm(n3*n4), nc=n3, nr=n4 )
  # Les biais
  b1 <- rnorm(n1)
  b2 <- rnorm(n2)
  b3 <- rnorm(n3)
  b4 <- rnorm(n4)
  # L'algorithme (backpropagation)
  r <- list()
  err <- rep(0, N)
  for (i in 1:N) {
    cat(paste("Itération", i, "\n"))
    res <- matrix(NA, nr=n, nc=dim(M)[2])
    for (j in 1:dim(M)[2]) {
      x <- M[,j] 
      # Calcul des valeurs des noeuds
      y0 <- x
      y1 <- f1( w1 %*% y0 + b1 )
      y2 <- f2( w2 %*% y1 + b2 )
      y3 <- f3( w3 %*% y2 + b3 )
      y4 <- f4( w4 %*% y3 + b4 )
      # Calcul des erreurs
      d4 <- x - y4
      d3 <- (t(w4) %*% d4) * (1 - y3^2)
      d2 <- (t(w3) %*% d3) * 1
      d1 <- (t(w2) %*% d2) * (1 - y1^2)
      # Mise à jour des poids et des biais
      dw4 <- d4 %*% t(y3)
      dw3 <- d3 %*% t(y2)
      dw2 <- d2 %*% t(y1)
      dw1 <- d1 %*% t(y0)
      w4 <- w4 + e * dw4
      w3 <- w3 + e * dw3
      w2 <- w2 + e * dw2
      w1 <- w1 + e * dw1
      b4 <- b4 + e * d4
      b3 <- b3 + e * d3
      b2 <- b2 + e * d2
      b1 <- b1 + e * d1
      res[,j] <- y2
      err[i] <- err[i] + sum(d4^2)
    }
    r[[i]] <- res
  }
  list(scores=res, errors=err, r=r)
}

r <- drnn(data.curve, 1, 4, N=30, e=.02)
plot(r$err, ylim=range(c(0,r$err)), type='l')

*

i <- order(drop(r$scores))
plot(t(data.curve)[,1:2])
lines(t(data.curve)[,1:2][i,], col='red')

*

Ça n'est pas exactement ce à quoi on s'attendait. Mais c'est quand même mieux que rien...

op <- par(mfrow=c(2,2))
for (k in c(1, 3, 7, 30)) {
  i <- order(drop(r$r[[k]]))
  plot(t(data.curve)[,1:2], main=paste("après",k,"itération(s)"))
  lines(t(data.curve)[,1:2][i,], col='red')
}
par(op)

*

Changer l'ordre dans lequel on présente les points au réseau de neurone n'a pas de réel effet.

op <- par(mfrow=c(2,4))
for (i in 1:4) {
  m <- data.curve[,sample(1:dim(data.curve)[2])]
  r <- drnn(m, 1, 4, N=30, e=.02)
  plot(r$err, ylim=range(c(0,r$err)), type='l')
  i <- order(drop(r$scores))
  plot(t(m)[,1:2])
  lines(t(m)[,1:2][i,], col='red')
}
par(op)

*

(Le graphique des erreurs est assez inquiétant : l'erreur est sensée baisser, itération après itération -- or elle augmente parfois...)

Regardons maintenant ce qui se passe si on met deux neurones dans la couche du milieu.

i <- sample(1:dim(data.curve)[2])
m <- data.curve[,i]
r <- drnn(m, 2, 8, N=30, e=.02)
plot(r$err, ylim=range(c(0,r$err)), type='l')
plot(t(r$scores)[i,], type='l')

*

(Oui, c'est sensé être une courbe...)

A FAIRE : regarder un peu mieux ce qui se passe quand on change l'un
des paramètres. A chaque fois, faire 4 graphiques : la décroissance
de l'erreur, le premier chemin, le dernier.

A FAIRE : vérifier que mon implémentation est correcte, en
l'utilisant dans une situation plus simple. (L'évolution de l'erreur
suggère une erreur quelque part...)

Autre idée

- Répartir les points dans 4 ou 5 classes (classification
  hiérarchique)
- Prendre le point médiant de ces classes.
- Le MST de ces 4 ou 5 points est-il ramifié ?

Sur nos exemples :

library(cluster)
library(ape)
mst.of.classification <- function (x, k=6, ...) {
  x <- t(x) 
  x <- t( t(x) - apply(x,2,mean) )
  r <- prcomp(x)
  y <- r$x
  u <- r$rotation
  r <- kmeans(y,k)
  z <- r$centers
  m <- mst(dist(z))
  plot(y[,1:2], ...)
  points(z[,1:2], col='red', pch=15)
  w <- which(m!=0)
  i <- as.vector(row(m))[w]
  j <- as.vector(col(m))[w]
  segments( z[i,1], z[i,2], z[j,1], z[j,2], col='red' )
}
set.seed(1)
mst.of.classification(data.curve, 6)

*

mst.of.classification(data.curve, 6)

*

op <- par(mfrow=c(2,2),mar=c(0,0,0,0))
for (k in c(4,6,10,15)) {
  mst.of.classification(data.curve, k, axes=F)
  box()
}
par(op)

*

op <- par(mfrow=c(2,2),mar=c(0,0,0,0))
for (k in c(4,6,10,15)) {
  mst.of.classification(data.noisy.curve, k, axes=F)
  box()
}
par(op)

*

op <- par(mfrow=c(2,2),mar=c(0,0,0,0))
for (k in c(4,6,10,15)) {
  mst.of.classification(data.broken.line, k, axes=F)
  box()
}
par(op)

*

op <- par(mfrow=c(2,2),mar=c(0,0,0,0))
for (k in c(4,6,10,15)) {
  mst.of.classification(data.noisy.broken.line, k, axes=F)
  box()
}
par(op)

*

Sur des données réelles, ça marche moins bien :

op <- par(mfrow=c(2,2),mar=c(0,0,0,0))
for (k in c(4,6,10,15)) {
  mst.of.classification(data.real, k, axes=F)
  box()
}
par(op)

*

Détails :

op <- par(mfrow=c(3,3),mar=c(0,0,0,0))
for (k in c(4:6)) {
  for (i in 1:3) {
    mst.of.classification(data.real, k, axes=F)
    box()
  }
}
par(op)

*

op <- par(mfrow=c(3,3),mar=c(0,0,0,0))
for (k in c(7:9)) {
  for (i in 1:3) {
    mst.of.classification(data.real, k, axes=F)
    box()
  }
}
par(op)

*

op <- par(mfrow=c(3,3),mar=c(0,0,0,0))
for (k in c(10:12)) {
  for (i in 1:3) {
    mst.of.classification(data.real, k, axes=F)
    box()
  }
}
par(op)

*

op <- par(mfrow=c(3,5),mar=c(0,0,0,0))
for (k in c(13:15)) {
  for (i in 1:3) {
    mst.of.classification(data.real, k, axes=F)
    box()
  }
}
par(op)

*

A FAIRE

Il y a quelque temps, j'ai vu passer un article sur la réduction de
la dimension (dans Rnews, ou une vignette dans la documentation de
R). Le relire.

Implémenter Isomap
(assez facile, utiliser MST.)

Regarder d'autres articles sur la réduction de la dimension.
google: dimensionality reduction

Isomap

A FAIRE : référence

A FAIRE : expliquer

Construction du graphe

# k: each point is linked to its k nearest neighbors
# eps: each point is linked to all its neighbors within a radius eps

isomap.incidence.matrix <- function (d, eps=NA, k=NA) {
  stopifnot(xor( is.na(eps), is.na(k) ))
  d <- as.matrix(d)
  if(!is.na(eps)) {
    im <- d <= eps
  } else {
    im <- apply(d,1,rank) <= k+1
    diag(im) <- F
  }
  im | t(im)
}
plot.graph <- function (im,x,y=NULL, ...) {
  if(is.null(y)) {
    y <- x[,2]
    x <- x[,1]
  }
  plot(x,y, ...)
  k <- which(  as.vector(im)  )
  i <- as.vector(col(im))[ k ]
  j <- as.vector(row(im))[ k ]
  segments( x[i], y[i], x[j], y[j], col='red' )
}

d <- dist(t(data.noisy.curve))
r <- princomp(t(data.noisy.curve))
x <- r$scores[,1]
y <- r$scores[,2]

plot.graph(isomap.incidence.matrix(d, k=5), x, y)

*

Pour l'instant, il y a un problème : le graphe obtenu n'est pas nécessairement connexe -- c'est très génant pour calculer des distances.

plot.graph(isomap.incidence.matrix(d, eps=quantile(as.vector(d), .05)), 
           x, y)

*

On voit aisément que le graphe obtenu est connexe si et seulement si il contient l'arbre couvrant minimal (MST) : on va donc simplement rajouter les arrêtes de let arbre couvrant minimal qui ne sont pas encore présentes.

isomap.incidence.matrix <- function (d, eps=NA, k=NA) {
  stopifnot(xor( is.na(eps), is.na(k) ))
  d <- as.matrix(d)
  if(!is.na(eps)) {
    im <- d <= eps
  } else {
    im <- apply(d,1,rank) <= k+1
    diag(im) <- F
  }
  im | t(im) | mst(d)
}
plot.graph(isomap.incidence.matrix(d, eps=quantile(as.vector(d), .05)), 
           x, y)

*

A FAIRE : mesure de la distance dans le graphe. C'est un problème de plus court chemin dans un graphe...

inf <- function (x,y) { ifelse(x<y,x,y) }
isomap.distance <- function (im, d) {
  d <- as.matrix(d)
  n <- dim(d)[1]
  dd <- ifelse(im, d, Inf)
  for (k in 1:n) {
    dd <- inf(dd, matrix(dd[,k],nr=n,nc=n) + matrix(dd[k,],nr=n,nc=n,byrow=T))
  }
  dd
}

isomap <- function (x, d=dist(x), eps=NA, k=NA) {
  im <- isomap.incidence.matrix(d, eps, k)
  dd <- isomap.distance(im,d)
  r <- list(x,d,incidence.matrix=im,distance=dd)
  class(r) <- "isomap"
  r    
}

r <- isomap(t(data.noisy.curve), k=5)
xy <- cmdscale(r$distance,2)   # long : compter une trentaine de secondes...
plot.graph(r$incidence.matrix, xy)

*

Dans un repère orthonormé :

plot.graph(r$incidence.matrix, xy, ylim=range(xy))

*

A FAIRE :

D'autres manières d'afficher les résultats : 
1. Données initiales, graphe
2. MDS
3. Données initiales, graphe, couleurs correspondant à la première
   coordonnée de l'analyse des coordonnées
4. La courbe ?

A FAIRE : appliquer ça à nos données

A FAIRE : le choix des paramètres (eps ou k)

Idée 2

On peut présenter ce problème comme un problème d'optimisation : on cherche les coordonnées de 5 points A1, A2, A3, A4, A5 afin de minimiser la somme des (carrés des) distances des points du nuage à la ligne brisée A1-A2-A3-A4-A5.

A FAIRE : pour vérifier que cet algorithme fonctionne, commencer par
lui donner
- un seul segment à retrouver, en dimension 1.
- un seul segment à retrouver, en dimension 2.
- une courbe (parabole, assez applatie) à approcher par un seul
  segment.
- une courbe (parabole) à approcher par deux segments. 
- enfin, seulement, la situation qui nous intéresse. 

distance.broken.line <- function (point, ligne) {
  # point : vecteur contenant les coordonnées du point du nuage dont
  #         on calcule la distance à la ligne
  # ligne : coordonnées, en colonnes, des points constituant la
  #         ligne brisée. 

  # Distance aux sommets de la ligne brisée
  d.points <- apply( (ligne-point)^2, 2, sum )

  # Position de la projection du point sur chacune des droites
  # constituant la ligne brisée
  v <- t(apply(ligne,1,diff))   # vecteurs A(i)A(i+1)
  w <- -ligne + point           # vecteurs A(i)M
  w <- w[, -dim(w)[2]]          # on enlève le dernier
  lambda <- apply(v*w, 2, sum) / apply(v^2,2,sum)
  lambda[ is.nan(lambda) ] <- 0

  # Distance aux droites constituant la ligne brisée
  d.segments <- apply( (w - t(lambda*t(v)))^2, 2, sum )
  d.segments <- d.segments[ lambda>0 & lambda<1 ]
 
  min(c( d.points, d.segments ))
}

# Vérifications : les distances suivantes sont (à peu près) nulles
stopifnot( distance.broken.line(p[,1], p) < 1e-6 )
stopifnot( distance.broken.line(.5*p[,1]+.5*p[,2], p) < 1e-6 )
# Il y a un gros problème de vitesse : il faut deux secondes pour
# calculer la distance sur cet exemple : 
sum(apply(data.noisy.broken.line, 2, distance.broken.line, p))

broken.line <- function (
    d,    # données
    np=4, # Nombre de sommets de la ligne brisée recherchée
    deb = as.vector(d[,sample(1:dim(d)[2], np)])
            # Valeurs de départ : ligne brisée passant par np points
            # du nuage, pris au hasard.
  ) {
  f <- function (x) {
    sum(apply(d, 2, distance.broken.line, matrix(x, nc=np) ))
  }
  # Très, très long
  r <- optim(deb, f, control=list(trace=1, maxit=50)) # Par défaut : 500 itérations
  list(par=r$par, method="optim", value=r$value, deb=deb, np=np, r=r)
}

r <- broken.line(data.noisy.broken.line)
pc <- princomp(t(data.noisy.broken.line))
plot(pc$scores[,1:2])

lines( (t(matrix(r$deb,nc=r$np) - pc$center) %*% pc$loadings)[,1:2], col='red',lwd=3,lty=3)
lines((t(matrix(r$par,nc=r$np) - pc$center) %*% pc$loadings)[,1:2], col='blue',lwd=3,lty=2)

*

# Exemple : un segment, en dimension un
l <- c(-5, 3, 2, -1)
deb <- c(1,2,3,4)
d <- rbind( seq(l[1],l[2],length=10), seq(l[3],l[4],length=10) )
r <- broken.line(d, np=2, deb=deb)
plot(t(d), xlim=c(-10,10), ylim=c(-10,10))
# Je m'embrouille dans l'ordre des paramètres à estimer...
for (i in 1:100) { lines(matrix(sample(r$par), nr=2) ) }

*

# valeurs de départ : certains points du nuage, pris au hasard
op <- par(mfrow=c(3,3), mar=c(0,0,0,0))
for (i in 1:9) {
  l <- runif(4, -8,8)
  d <- rbind( seq(l[1],l[2],length=10), seq(l[3],l[4],length=10) )    
  r <- broken.line(d, np=2)
  plot(t(d), xlim=c(-10,10), ylim=c(-10,10),
       xlab='', ylab='', axes=F)
  box()
  lines( r$par[c(1,3)], r$par[c(2,4)], col='red' )
}
par(op)

*

# Valeurs de départ complètement aléatoires
op <- par(mfrow=c(3,3), mar=c(0,0,0,0))
for (i in 1:9) {
  l <- runif(4, -8,8)
  deb <- runif(4, -8,8)
  d <- rbind( seq(l[1],l[2],length=10), seq(l[3],l[4],length=10) )    
  r <- broken.line(d, np=2, deb=deb)
  plot(t(d), xlim=c(-10,10), ylim=c(-10,10),
       xlab='', ylab='', axes=F)
  box()
  lines( r$par[c(1,3)], r$par[c(2,4)], col='red' )
  lines( deb[c(1,3)], deb[c(2,4)], col='blue', lty=2 )
}
par(op)

*

Aaaaaaahhhhhhhhhhhh... Ca ne converge pas vraiment. Essayons d'insister, en lançant l'algorithme plusieurs fois,

op <- par(mfrow=c(3,3), mar=c(0,0,0,0))
for (i in 1:9) {
  l <- runif(4, -8,8)
  deb <- runif(4, -8,8)
  d <- rbind( seq(l[1],l[2],length=10), seq(l[3],l[4],length=10) )    
  plot(t(d), xlim=c(-10,10), ylim=c(-10,10),
       xlab='', ylab='', axes=F)
  box()
  lines( deb[c(1,3)], deb[c(2,4)], col='blue', lty=2 )
  for (j in 1:10) {    
    r <- broken.line(d, np=2, deb=deb)
    lines( r$par[c(1,3)], r$par[c(2,4)], col='red' )
    deb <- r$par
  }
}
par(op)
%--

Non, ça ne converge décidément pas.

A FAIRE : regarder quelle est la valeur de la distance à la fin de
l'algorithme...
J'obtient des valeurs extrèmenent faibles, alors que le segment est
assez loin des points : pourquoi ???
(Ce bug pourrait expliquer les problèmes de convergence...)

Il y a un autre problème : la solution qu'on cherche n'est pas unique. Si un segment est solution de notre problème d'optimisation, alors tout segment plus long le sera aussi.

Ca converge trop lentement pour qu'on ait un résultat. Essayons avec la fonction nlm (après réflexions, il semblerait qu'"optim" soit efficace sur des fonctions "méchantes" et "nlm" sur des fonctions gentilles -- or la fonction qu'on cherche à minimiser me semble quadratique par morceaux, avec un unique minimum local, qui est donc aussi global).

# Encore plus long...
r <- nlm(f, deb, iterlim=20)   # Par défaut, iterlim=100...
pc <- princomp(t(data.noisy.broken.line))
plot(pc$scores[,1:2])
lines( (t(matrix(deb, nc=np) - pc$center) %*% pc$loadings)[,1:2], col='red', lwd=3, lty=3 )
lines((t(matrix(r$estimate,nc=np) - pc$center) %*% pc$loadings)[,1:2], col='blue', lwd=3, lty=2)

On peut tenter d'accélérer ces convergences en traitant séparément les dimensions.

NON, on ne peut pas...
A FAIRE

Dans l'exemple précédent, la courbe était constituée de 4 segments mais on demandait une courbe constituée de 3 segments : si on en avait demandé plus, on aurait (probablement : les calculs sont longs je n'ai pas eu la patience d'attendre...) obtenu des segments de longueur nulle, sur lesquels la fonction de calcul des distance risque d'avoir des problèmes. Je suggère donc d'ajouter une pénalité à la fonction qu'on cherche à minimiser afin que les segments aient des longueurs comparables.

# J'ajoute la somme des inverses des carrés des longueurs des segments
f <- function (x) {
  sum(apply(data.noisy.broken.line, 2, distance, matrix(x, nc=np) )) +
  sum( 1/apply(t(apply(matrix(x,nc=np), 1,diff))^2, 2, sum) )
}

A FAIRE : un exemple de calcul avec 6 ou 7 points (alors que la
droite brisée en compte 5).

Idée 2 bis

Idem, avec des courbes (par exemple, des splines).

A FAIRE

Idée 3 : Problème du voyageur de commerce

MST

library(ape)
my.plot.mst <- function (d) {
  r <- mst(dist(t(d)))
  d <- prcomp(t(d))$x[,1:2]
  plot(d)
  n <- dim(r)[1]
  w <- which(r!=0)
  i <- as.vector(row(r))[w]
  j <- as.vector(col(r))[w]
  segments( d[i,1], d[i,2], d[j,1], d[j,2], col='red' )
}
my.plot.mst(data.broken.line)

*

my.plot.mst(data.noisy.broken.line)

*

my.plot.mst(data.curve)

*

my.plot.mst(data.noisy.curve)

*

my.plot.mst(data.real)

*

my.plot.mst(t(data.real.3d))

*

Arbre couvrant minimal élagué

# Donne la liste des chemins des noeuds de branchement vers les
# feuilles
chemins.vers.les.feuilles <- function (G) {
  nodes <- which(apply(G,2,sum)>2)
  leaves <- which(apply(G,2,sum)==1)
  res <- list()
  for (a in nodes) {
    for (b in which(G[a,]>0)) {
      if (! b %in% nodes) {
        res <- append(res,list(c(a,b)))
      }
    }
  }
  chemins.vers.les.feuilles.suite(G, nodes, leaves, res)
}
# Derniere coordonnée d'un vecteur
end1 <- function (x) {
  n <- length(x)
  x[n]
}
# Deux dernières coordonnées d'un vecteur
end2 <- function (x) {
  n <- length(x)
  x[c(n-1,n)]
}
chemins.vers.les.feuilles.suite <- function (G, nodes, leaves, res) {
  new <- list()
  done <- T
  for (ch in res) {
    if( end1(ch) %in% nodes ) {
      # Pass
    } else if ( end1(ch) %in% leaves ) {
      new <- append(new, list(ch))
    } else {
      done <- F
      a <- end2(ch)[1]
      b <- end2(ch)[2]
      for (x in which(G[b,]>0)) {
        if( x != a ){
          new <- append(new, list(c( ch, x )))
        }
      }
    }
  }
  if(done) {
    return(new)
  } else {
    return(chemins.vers.les.feuilles.suite(G,nodes,leaves,new))
  }
} 

G <- matrix(c(0,1,0,0, 1,0,1,1, 0,1,0,0, 0,1,0,0), nr=4)
chemins.vers.les.feuilles(G)

# Calcul de la longueur d'un chemin
longueur.chemin <- function (chemin, d) {
  d <- as.matrix(d)
  n <- length(chemin)
  i <- chemin[ 1:(n-1) ]
  j <- chemin[ 2:n ]
  if (n==2) {
    d[i,j]
  } else {
    sum(diag(d[i,][,j]))
  }
}

G <- matrix(c(0,1,0,0, 1,0,1,1, 0,1,0,0, 0,1,0,0), nr=4)
d <- matrix(c(0,2,4,3, 2,0,2,1, 4,2,0,3, 3,1,3,0), nr=4)
chemins <- chemins.vers.les.feuilles(G)
chemins
l <- sapply(chemins, longueur.chemin, d)
l
stopifnot( l == c(2,2,1) )

élague <- function (G0, d0) {
  d0 <- as.matrix(d0)
  G <- G0
  d <- d0
  res <- 1:dim(d)[1]
  chemins <- chemins.vers.les.feuilles(G)
  while (length(chemins)>0) {
    longueurs <- sapply(chemins, longueur.chemin, d)
    # Numéro du chemin le plus court
    i <- which( longueurs == min(longueurs) )[1]
    cat(paste("Removing", paste(res[chemins[[i]]],collapse=' '), "length =", longueurs[i],"\n"))
    # Noeuds à enlever
    j <- chemins[[i]] [-1]
    res <- res[-j]
    G <- G[-j,][,-j]
    d <- d[-j,][,-j]
    cat(paste("Removing", paste(j), "\n" ))
    cat(paste("", paste(res, collapse=' '), "\n"))
    chemins <- chemins.vers.les.feuilles(G)      
  }
  res
}

library(ape)
my.plot.mst <- function (x0) {
  cat("Plotting the points\n")
  x <- prcomp(t(x0))$x[,1:2]
  plot(x)
  cat("Computing the distance matrix\n")
  d <- dist(t(x0))
  cat("Computing the MST (Minimum Spanning Tree)\n")
  G <- mst(d)
  cat("Plotting the MST\n")
  n <- dim(G)[1]
  w <- which(G!=0)
  i <- as.vector(row(G))[w]
  j <- as.vector(col(G))[w]
  segments( x[i,1], x[i,2], x[j,1], x[j,2], col='red' )
  cat("Pruning the tree\n")
  k <- élague(G,d)
  cat("Plotting the pruned tree\n")
  x <- x[k,]
  G <- G[k,][,k]
  n <- dim(G)[1]
  w <- which(G!=0)
  i <- as.vector(row(G))[w]
  j <- as.vector(col(G))[w]
  segments( x[i,1], x[i,2], x[j,1], x[j,2], col='red', lwd=3 )        
}

my.plot.mst(data.noisy.broken.line)

*

my.plot.mst(data.noisy.curve)

*

my.plot.mst(data.real)

*

my.plot.mst(t(data.real.3d))

*

Remarque : en analyse d'image, on utilise parfois une simplification de cet algorithme (qui s'appelle encore "élagage") pour se débarasser des ramifications dans le squelette d'une image : on se contente de grignoter deux ou trois segments à partir de chaque feuille.

A FAIRE : une référence

TSP

Le problème du voyageur de commerce est le suivant : on dispose d'un nuage de points (dans un espace métrique) et on cherche un chemin, de longueur minimale, qui passe par chacun d'eux. Il s'agit donc de trouver un ordre sur un ensemble de points qui minimise une certaine fonction.

Présenté ainsi, ce problème a déjà beaucoup d'applications (par exemple, établir des circuits de livraison ; par exemple, définir le trajet d'un fer à souder, dans une chaine de montage d'appareils électroniques), mais beaucoup d'autres problèmes qui semblent ne rien à voir avec le TSP peuvent se reformuler en termes de TSP (par exemple, des problèmes de cartographie génétique : voir le livre de Setubal -- que je n'ai jamais ouvert).

On peut appliquer un algorithme de résolution du TSP à notre nuage de points, on obtient un ordre sur ces points, que l'on peut voir comme une ligne brisée. Ensuite, on "lisse" cette ligne brisée : on obtient une courbe, paramétrée par [0,1] : ce paramètre est l'indice recherché. Enfin, étant donné un nouveau point, il suffit de le projeter sur cette courbe pour obtenir la valeur du paramètre.

Je constate que des algorithmes de résolution du TSP ne sont pas implémentés sous R : il va nous falloir programmer...

TSP : descente

Voici une implémentation de la descente simple.

longueur <- function (d,o) {
  n <- length(o)
  sum(diag( d [o[1:(n-1)],] [,o[2:n]] ))
}
tsp.descent <- function (d, N=1000) {
  # d : matrice des distances. 
  d <- as.matrix(d)
  n <- dim(d)[1]
  o <- sample(1:n)
  v <- longueur(d,o)
  k <- 0
  res <- list()
  k.res <- c()
  l.res <- c(v)
  while (k<N) {
    i <- sample(1:n, 2)
    oo <- o
    oo[ i[1] ] <- o[ i[2] ]
    oo[ i[2] ] <- o[ i[1] ]
    w <- longueur(d,oo)
    if (w<v) {
      v <- w
      o <- oo
      res <- append(res, list(o))
      k.res <- append(k.res, k)
      l.res <- append(l.res, v)
      k <- 0
    } else {
      k <- k+1
    }
  }
  list(o=o, détails=res, k=k.res, longueur=v, longueurs=l.res)
}

n <- 100
x <- matrix(runif(2*n), nr=n)
r <- tsp.descent(dist(x))
o <- r$o
plot(x)
lines(x[o,], col='red')

*

op <- par(mfrow=c(2,2))
n <- length(r$détails)  
for (i in floor(c(1, n/4, n/2, n))) {
  plot(x, main=paste("n =",i))
  lines(x[r$détails[[i]],], col='red')
}
par(op)

*

La valeur de N que nous avons prise est insuffisante :

plot(r$k)

*

n <- 100
x <- seq(0,6, length=n)
x <- sample(x)
x <- cbind(cos(x), sin(x))
r <- tsp.descent(dist(x), N=10000)
op <- par(mfrow=c(2,2))
n <- length(r$détails)  
for (i in floor(c(1, n/4, n/2, n))) {
  plot(x, main=paste("n =",i))
  lines(x[r$détails[[i]],], col='red')
}
par(op)

*

plot(r$k)

*

On peut tenter d'améliorer les résultats en lançant l'algorithme plusieurs fois.

# Très long...
op <- par(mfrow=c(3,3))
for (i in 1:9) {
  r <- tsp.descent(dist(x), N=1000)
  plot(x, main=signif(r$longueur))
  lines( x[r$o,], col='red', type='l' )
}
par(op)

*

Ça n'est toujours pas concluant.

TSP : Recuit simulé

On peut modifier l'algorithme précédent en acceptant un nouveau chemin moins bon avec une probabilité initialement élevée (0.8) et qui décroit progressivement.

tsp.recuit <- function (d, N=1000, taux=.99) {
  # d : matrice des distances. 
  d <- as.matrix(d)
  n <- dim(d)[1]
  o <- sample(1:n)
  v <- longueur(d,o)
  # Calcul de la température initiale
  dE <- NULL
  j <- 0
  for (i in 1:100) {
    i <- sample(1:n, 2)
    oo <- o
    oo[ i[1] ] <- o[ i[2] ]
    oo[ i[2] ] <- o[ i[1] ]
    w <- longueur(d,oo)
    if (w<v) {
      dE <- append(dE,abs(w-v))
      j <- j+1        
    }
  }
  print(dE)
  T <- - max(dE) / log(.8)
  print(T)
  # Initialisations
  k <- 0
  res <- list()
  k.res <- c()
  l.res <- c(v)
  t.res <- c(T)
  p.res <- c()
  while (k<N) {
    i <- sample(1:n, 2)
    oo <- o
    oo[ i[1] ] <- o[ i[2] ]
    oo[ i[2] ] <- o[ i[1] ]
    w <- longueur(d,oo)
    p.res <- append(p.res, exp((v-w)/T))
    if ( runif(1) < exp((v-w)/T) ) {
      v <- w
      o <- oo
      res <- append(res, list(o))
      k.res <- append(k.res, k)
      l.res <- append(l.res, v)
      k <- 0
      T <- T*taux
      t.res <- append(t.res, T)
    } else {
      k <- k+1
    }
  }
  list(o=o, détails=res, k=k.res, longueur=v, longueurs=l.res, T=t.res, p=p.res)
}

n <- 100
x <- seq(0,5, length=n)
x <- sample(x)
x <- cbind(cos(x), sin(x))
r <- tsp.recuit(dist(x), N=1000, taux=.995)
op <- par(mfrow=c(2,2))
n <- length(r$détails)  
for (i in floor(c(1, n/4, n/2, n))) {
  plot(x, main=paste("n =",i))
  lines(x[r$détails[[i]],], col='red')
}
par(op)

*

op <- par(mfrow=c(2,2))
plot(r$k)
plot(r$longueurs, main="Longueurs")
plot(r$T, main="Température")
plot(r$p, ylim=0:1, main="Probabilités")
par(op)

*

Programmes extérieurs : heuristiques et algorithmes exacts

On sait actuellement résoudre, de manière exacte, des problèmes du voyageur de commerce avec plusieurs milliers de villes, en exprimant le problème sous forme d'un programme linéaire. (C'est un peu délicat à décrire, car les contraintes sont en nombre exponentiel : on commence par résoudre le problème sans aucune contrainte, on regarde quelles sont les contraintes violées (c'est facile : la solution comporte des cycles ; ces cycles correspondent aux contraintes), et on les ajoute ; on recommence ensuite, jusqu'à ce que la solution soit correcte.)

A FAIRE : expliquer plus en détails comment on fait...

Voici quelques logiciels qui permettent de le résoudre.

Concorde est un solveur de TSP (développé à Princeton, librement utilisable dans un cadre universitaire) qui repose sur un solveur de programmes linéaires, comme QSoft (même licence) ou CPlex (commercial).

A FAIRE : URL de Concorde et QSoft

La documentation du format des fichiers : 
http://www.informatik.uni-heidelberg.de/groups/comopt/software/TSPLIB95/DOC.PS

Le site http://www.branchandcut.org/ liste des logiciels de Branch and Cut. Symphony est un logiciel (open source) de résolution de problèmes par Branch & Cut -- entres autres, le TSP. Il suppose aussi qu'on dispose d'un solveur de programmes linéaires, par exemple le CLP, Coin LP Solver (COIN, COmputational INfrasctructure for Operations Research, dépend d'IBM).

A FAIRE : URL de Symphony.
http://www.coin-or.org/

Voici un exemple d'utilisation de "Linkern" (ce programme utilise une heuristique, mais il est un peu plus rapide) pour résoudre un TSP.

print.tsp <- function (x, d=dist(x), f="") {
  ## BEWARE: The documentation states "All explicit [weight] data is
  ##         integral"
  ## If the best tour has length 0, the problem may come from there.
  d <- round(as.dist(d))
  n <- dim(x)[1]
  cat("TYPE : TSP\n", file=f) # Création du fichier
  cat("DIMENSION : ", file=f, append=T)
  cat(n, file=f, append=T)
  cat("\n", file=f, append=T)
  cat("EDGE_WEIGHT_TYPE : EXPLICIT\n", file=f, append=T)
  cat("EDGE_WEIGHT_FORMAT : UPPER_ROW\n", file=f, append=T)
  cat("DISPLAY_DATA_TYPE : NO_DISPLAY\n", file=f, append=T)
  cat("EDGE_WEIGHT_SECTION\n", file=f, append=T)
  cat(paste(as.vector(d), collapse=" "), file=f, append=T)    
  cat("\n", file=f, append=T)
  cat("EOF\n", file=f, append=T)
}
tsp.plot.linkern <- function (x, d=dist(x), ...) {
  print.tsp(x, d, f="tmp.tsp")
  system(paste("./linkern", "-o", "tmp.tsp_result", "tmp.tsp"))
  ij <- 1+read.table("tmp.tsp_result", skip=1)[,1:2]
  xy <- prcomp(x)$x[,1:2]
  plot(xy, ...)
  segments( xy[,1][ ij[,1] ], 
            xy[,2][ ij[,1] ], 
            xy[,1][ ij[,2] ], 
            xy[,2][ ij[,2] ],
            col='red' )
  ij
}

op <- par(mfrow=c(3,3), mar=c(0,0,0,0))
for (i in 1:9) {  
  n <- 50
  k <- 3
  x <- matrix(rnorm(n*k), nr=n, nc=k)
  tsp.plot.linkern(x, d=round(100*dist(x)), axes=F)
  box()
}
par(op)

*

Utilisons maintenant Concorde.

tsp.plot.concorde <- function (x, d=dist(x), plot=T, ...) {
  print.tsp(x, d, f="tmp.tsp")
  system(paste("./concorde", "tmp.tsp"))
  i <- scan("tmp.sol")[-1]
  ij <- 1+matrix(c(i, i[-1], i[1]), nc=2)
  xy <- prcomp(x)$x[,1:2]
  if (plot) {
    plot(xy, ...)
    n <- dim(ij)[1]
    segments( xy[,1][ ij[,1] ], 
              xy[,2][ ij[,1] ], 
              xy[,1][ ij[,2] ], 
              xy[,2][ ij[,2] ],
              col=rainbow(n) )
  }
  ij
}
op <- par(mfrow=c(3,3), mar=c(0,0,0,0))
for (i in 1:9) {  
  n <- 50
  k <- 3
  x <- matrix(rnorm(n*k), nr=n, nc=k)
  tsp.plot.concorde(x, d=round(100*dist(x)), axes=F)
  box()
}
par(op)

*

Recherche d'un chemin ouvert

Si on applique cela à nos données simulées, il vient :

tsp.plot.concorde(t(data.broken.line))

*

Il y a un segment en trop, car l'algorithme recherche un chemin fermé -- nous, non.

Sur des données bruitées, c'est encore pire : le chemin est parcourru dans un sens, puis dans l'autre...

tsp.plot.concorde(t(data.noisy.broken.line))

*

On le voit mieux en lissant la courbe :

x <- t(data.noisy.broken.line)
ij <- tsp.plot.concorde(x, plot=F)
xy <- prcomp(x)$x[,1:2]
y <- xy[ij[,1],]
x <- 1:dim(ij)[1]
plot(xy)
x1 <- predict(loess(y[,1]~x, span=.2))
x2 <- predict(loess(y[,2]~x, span=.2))
n <- length(x1)
segments(x1[-n], x2[-n], x1[-1], x2[-1], col=rainbow(n-1), lwd=3)

*

On peut transformer un problème du voyageur de commerce ouvert en un problème fermé en ajoutant un sommet dont la distance à tous les autres est nulle (ou dont la distance à tous les noeuds est la même).

print.open.tsp <- function (x, d=dist(x), f="") {
  ## BEWARE: The documentation states "All explicit [weight] data is
  ##         integral"
  ## If the best tour has length 0, the problem may come from there.
  d <- as.matrix(d)
  d <- cbind(0,rbind(0,d))
  d <- round(as.dist(d))
  n <- dim(x)[1] + 1
  cat("TYPE : TSP\n", file=f) # Création du fichier
  cat("DIMENSION : ", file=f, append=T)
  cat(n, file=f, append=T)
  cat("\n", file=f, append=T)
  cat("EDGE_WEIGHT_TYPE : EXPLICIT\n", file=f, append=T)
  cat("EDGE_WEIGHT_FORMAT : UPPER_ROW\n", file=f, append=T)
  cat("DISPLAY_DATA_TYPE : NO_DISPLAY\n", file=f, append=T)
  cat("EDGE_WEIGHT_SECTION\n", file=f, append=T)
  cat(paste(as.vector(d), collapse=" "), file=f, append=T)    
  cat("\n", file=f, append=T)
  cat("EOF\n", file=f, append=T)
}
tsp.plot.concorde.open <- function (x, d=dist(x), plot=T, smooth=F, span=.2,...) {
  print.open.tsp(x, d, f="tmp.tsp")
  system(paste("./concorde", "tmp.tsp"))
  i <- scan("tmp.sol")[-1]  # On enlève le nombre de 
  i <- i[-1]                # On enlève le premier noeud : 0
  ij <- matrix(c(i[-length(i)], i[-1] ), nc=2)
  xy <- prcomp(x)$x[,1:2]
  if (plot) {
    if (smooth) {
      y <- xy[ij[,1],]
      x <- 1:dim(ij)[1]
      plot(xy, ...)
      x1 <- predict(loess(y[,1]~x, span=span))
      x2 <- predict(loess(y[,2]~x, span=span))
      n <- length(x1)
      segments(x1[-n], x2[-n], x1[-1], x2[-1], col=rainbow(n-1), lwd=3)
    } else {
      plot(xy, ...)
      n <- dim(ij)[1]
      segments( xy[,1][ ij[,1] ], 
                xy[,2][ ij[,1] ], 
                xy[,1][ ij[,2] ], 
                xy[,2][ ij[,2] ],
                col=rainbow(n) )
    }
  }
  ij
}
x <- t(data.noisy.broken.line)
tsp.plot.concorde.open(x)

*

tsp.plot.concorde.open(x, smooth=T)

*

Application à nos données

tsp.plot.concorde.open(t(data.curve), smooth=T)

*

tsp.plot.concorde.open(t(data.noisy.curve), smooth=T)

*

tsp.plot.concorde.open(t(data.real), smooth=F)

*

tsp.plot.concorde.open(t(data.real), smooth=T)

*

op <- par(mfrow=c(2,2))
for (s in c(.2, .3, .4, .5)) {
  tsp.plot.concorde.open(t(data.real), smooth=T, span=s)
}
par(op)

*

tsp.plot.concorde.open(data.real.3d, smooth=F)

*

tsp.plot.concorde.open(data.real.3d, smooth=T)

*

tsp.plot.concorde.open(data.real.3d, smooth=T, span=.5)

*

A FAIRE

Encore une autre idée

On prend notre nuage de points, on munit chacun d'une masse et ils
s'attirent. On peut choisir le type de loi d'attraction : par
exemple, une force inversement proportionnelle au carré de la
distance qui les sépare, dans un espace de dimension 3. On simule
ensuite l'évolution de ce système (mais pas jusqu'à l'équilibre : on
s'arrête avant. Au bout d'un moment, les points forment bien une
courbe.

[C'est l'un des algorithmes qu'ion utilise pour dessiner des noeuds
-- mais c'est très délicat à paramétrer : on risque de se trouver
avec des points qui s'effondrent les uns sur les autres ou qui
s'envoient à l'infini.]

Il y a plusieurs choix délicats à faire : 
1. Le choix de la loi d'attraction
1bis. La dimension de l'espace (dans lequel les points se trouvent
  et dans lequel on calcule les distances) a peut-être une influence.
2. La simulation de l'évolution (ni trop lent, ni trop rapide)
3. Quand s'arrêter ?

Idée pour l'arrêt : regarder la "ramification" de l'arbre couvrant
minimal (je défini la ramification d'un arbre comme le nombre de
noeuds qui ne sont pas sur le chemin le plus long -- mais je ne sais
pas si ça se calcule facilement).

(Pour un arbre binaire, ça se calcule facilement)
Enraciner l'arbre en un noeud quelconque, pour chacune des trois
branches qui en partent, prendre les deux noeuds les plus profonds,
le chemin reliant les deux noeuds les plus profonds parmi ces six
est le plus long. 
(sous toutes réserves)

Algorithme : 
Tant qu'il reste des noeuds d'arité au moins trois : 
  parmi les branches partant de ces noeuds, retirer la plus petite

Encore une idée

Assigner une valeur aléatoire de l'indice à chaque point du nuage
En déduire un ordre sur les points
En déduire la courbe sur laquelle ils sont
Lisser cette courbe
A l'aide de cette courbe, donner une nouvelle valeur à l'indice
Itérer

Encore une autre idée

Répartir les points en quelques groupes
Trouver une droite (un segment) pour chacun de ces groupes
Essayer de recoller (comment ???)

Idée 1 bis

A FAIRE : svm si on connait l'indice de chaque patient.

A FAIRE

Ajouter le temps de calcul
Tenter de reconstruire une ligne brisée à 5 sommet avec plus de 5 points
Légendes

Pour les réseaux de neurones, présenter les choses un peu
différemment : 
  Algorithme "en ligne", qui prend toujours de nouvelles données 
    (c'est facile, car on sait comment nos données ont été obtenues,
    on peut dons en avoir un nombre infini).
  Commencer par implémenter une ACP linéaire et comparer avec l'ACP. 

Algorithmes d'optimisation :
  Commencer par une situation simple : un seul segment, sans bruit,
    en dimension deux (ou même en dimension un). 
  En particulier, cela permettra de vérifier que mon code fonctionne
    correctement.

A FAIRE : pour les exemples simulés, diviser les points en quatre
groupes et les indiquer par des couleurs.
Ainsi, on pourra voir si on est sensé voir quelque-chose ou pas.
Idéalement, on pourrait aussi mettre tous ces graphiques dans une
même fonction, qu'on appellerait pour chacun des exemples.
Pour cela, il faudrait mettre les données réelles en premier.

Vincent Zoonekynd
<zoonek@math.jussieu.fr>
latest modification on Mon Jul 26 09:33:43 CEST 2004