PageRenderTime 54ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/code/mcrbm/hmc.py

https://github.com/delallea/DeepLearningTutorials
Python | 393 lines | 232 code | 18 blank | 143 comment | 0 complexity | 3eeab9487b6b2d04f9851d5f170a9c78 MD5 | raw file
  1. """
  2. TODO
  3. """
  4. import numpy
  5. from theano import function, shared
  6. from theano import tensor as TT
  7. import theano
  8. sharedX = lambda X, name : \
  9. shared(numpy.asarray(X, dtype=theano.config.floatX), name=name)
  10. def kinetic_energy(vel):
  11. """
  12. Returns the kinetic energy associated with the given velocity and mass of 1.
  13. Parameters
  14. ----------
  15. vel: theano matrix
  16. Symbolic matrix whose rows are velocity vectors.
  17. Returns
  18. -------
  19. return: theano vector
  20. Vector whose i-th entry is the kinetic entry associated with vel[i].
  21. """
  22. return 0.5 * (vel**2).sum(axis=1)
  23. def hamiltonian(pos, vel, energy_fn):
  24. """
  25. Returns the Hamiltonian (sum of potential and kinetic energy) for the given
  26. velocity and position.
  27. Parameters
  28. ----------
  29. pos: theano matrix
  30. Symbolic matrix whose rows are position vectors.
  31. vel: theano matrix
  32. Symbolic matrix whose rows are velocity vectors.
  33. energy_fn: python function
  34. Python function, operating on symbolic theano variables, used to compute
  35. the potential energy at a given position.
  36. Returns
  37. -------
  38. return: theano vector
  39. Vector whose i-th entry is the Hamiltonian at position pos[i] and
  40. velocity vel[i].
  41. """
  42. # assuming mass is 1
  43. return energy_fn(pos) + kinetic_energy(vel)
  44. def metropolis_hastings_accept(energy_prev, energy_next, s_rng):
  45. """
  46. Performs a Metropolis-Hastings accept-reject move.
  47. Parameters
  48. ----------
  49. energy_prev: theano vector
  50. Symbolic theano tensor which contains the energy associated with the
  51. configuration at time-step t.
  52. energy_next: theano vector
  53. Symbolic theano tensor which contains the energy associated with the
  54. proposed configuration at time-step t+1.
  55. s_rng: theano.tensor.shared_randomstreams.RandomStreams
  56. Theano shared random stream object used to generate the random number
  57. used in proposal.
  58. Returns
  59. -------
  60. return: boolean
  61. True if move is accepted, False otherwise
  62. """
  63. ediff = energy_prev - energy_next
  64. return (TT.exp(ediff) - s_rng.uniform(size=energy_prev.shape)) >= 0
  65. def simulate_dynamics(initial_pos, initial_vel, stepsize, n_steps, energy_fn):
  66. """
  67. Return final (position, velocity) obtained after an `n_steps` leapfrog
  68. updates, using Hamiltonian dynamics.
  69. Parameters
  70. ----------
  71. initial_pos: shared theano matrix
  72. Initial position at which to start the simulation
  73. initial_vel: shared theano matrix
  74. Initial velocity of particles
  75. stepsize: shared theano scalar
  76. Scalar value controlling amount by which to move
  77. energy_fn: python function
  78. Python function, operating on symbolic theano variables, used to compute
  79. the potential energy at a given position.
  80. Returns
  81. -------
  82. rval1: theano matrix
  83. Final positions obtained after simulation
  84. rval2: theano matrix
  85. Final velocity obtained after simulation
  86. """
  87. def leapfrog(pos, vel, step):
  88. """
  89. Inside loop of Scan. Performs one step of leapfrog update, using
  90. Hamiltonian dynamics.
  91. Parameters
  92. ----------
  93. pos: theano matrix
  94. in leapfrog update equations, represents pos(t), position at time t
  95. vel: theano matrix
  96. in leapfrog update equations, represents vel(t - stepsize/2),
  97. velocity at time (t - stepsize/2)
  98. step: theano scalar
  99. scalar value controlling amount by which to move
  100. Returns
  101. -------
  102. rval1: [theano matrix, theano matrix]
  103. Symbolic theano matrices for new position pos(t + stepsize), and
  104. velocity vel(t + stepsize/2)
  105. rval2: dictionary
  106. Dictionary of updates for the Scan Op
  107. """
  108. # from pos(t) and vel(t-stepsize/2), compute vel(t+stepsize/2)
  109. dE_dpos = TT.grad(energy_fn(pos).sum(), pos)
  110. new_vel = vel - step * dE_dpos
  111. # from vel(t+stepsize/2) compute pos(t+stepsize)
  112. new_pos = pos + step * new_vel
  113. return [new_pos, new_vel],{}
  114. # compute velocity at time-step: t + stepsize/2
  115. initial_energy = energy_fn(initial_pos)
  116. dE_dpos = TT.grad(initial_energy.sum(), initial_pos)
  117. vel_half_step = initial_vel - 0.5*stepsize*dE_dpos
  118. # compute position at time-step: t + stepsize
  119. pos_full_step = initial_pos + stepsize * vel_half_step
  120. # perform leapfrog updates: the scan op is used to repeatedly compute
  121. # vel(t + (m-1/2)*stepsize) and pos(t + m*stepsize) for m in [2,n_steps].
  122. (all_pos, all_vel), scan_updates = theano.scan(leapfrog,
  123. outputs_info=[
  124. dict(initial=pos_full_step),
  125. dict(initial=vel_half_step),
  126. ],
  127. non_sequences=[stepsize],
  128. n_steps=n_steps-1)
  129. final_pos = all_pos[-1]
  130. final_vel = all_vel[-1]
  131. # NOTE: Scan always returns an updates dictionary, in case the scanned function draws
  132. # samples from a RandomStream. These updates must then be used when compiling the Theano
  133. # function, to avoid drawing the same random numbers each time the function is called. In
  134. # this case however, we consciously ignore "scan_updates" because we know it is empty.
  135. assert not scan_updates
  136. # The last velocity returned by scan is vel(t + (n_steps-1/2)*stepsize)
  137. # We therefore perform one more half-step to return vel(t + n_steps*stepsize)
  138. energy = energy_fn(final_pos)
  139. final_vel = final_vel - 0.5 * stepsize * TT.grad(energy.sum(), final_pos)
  140. # return new proposal state
  141. return final_pos, final_vel
  142. def hmc_move(s_rng, positions, energy_fn, stepsize, n_steps):
  143. """
  144. This function performs one-step of Hybrid Monte-Carlo sampling. We start by
  145. sampling a random velocity from a univariate Gaussian distribution, perform
  146. `n_steps` leap-frog updates using Hamiltonian dynamics and accept-reject
  147. using Metropolis-Hastings.
  148. Parameters
  149. ----------
  150. s_rng: theano shared random stream
  151. Symbolic random number generator used to draw random velocity and
  152. perform accept-reject move.
  153. positions: shared theano matrix
  154. Symbolic matrix whose rows are position vectors.
  155. energy_fn: python function
  156. Python function, operating on symbolic theano variables, used to compute
  157. the potential energy at a given position.
  158. stepsize: shared theano scalar
  159. Shared variable containing the stepsize to use for `n_steps` of HMC
  160. simulation steps.
  161. n_steps: integer
  162. Number of HMC steps to perform before proposing a new position.
  163. Returns
  164. -------
  165. rval1: boolean
  166. True if move is accepted, False otherwise
  167. rval2: theano matrix
  168. Matrix whose rows contain the proposed "new position"
  169. """
  170. # sample random velocity
  171. initial_vel = s_rng.normal(size=positions.shape)
  172. # perform simulation of particles subject to Hamiltonian dynamics
  173. final_pos, final_vel = simulate_dynamics(
  174. initial_pos = positions,
  175. initial_vel = initial_vel,
  176. stepsize = stepsize,
  177. n_steps = n_steps,
  178. energy_fn = energy_fn)
  179. # accept/reject the proposed move based on the joint distribution
  180. accept = metropolis_hastings_accept(
  181. energy_prev = hamiltonian(positions, initial_vel, energy_fn),
  182. energy_next = hamiltonian(final_pos, final_vel, energy_fn),
  183. s_rng=s_rng)
  184. return accept, final_pos
  185. def hmc_updates(positions, stepsize, avg_acceptance_rate, final_pos, accept,
  186. target_acceptance_rate, stepsize_inc, stepsize_dec,
  187. stepsize_min, stepsize_max, avg_acceptance_slowness):
  188. """
  189. This function is executed after `n_steps` of HMC sampling (`hmc_move`
  190. function). It creates the updates dictionary used by the `simulate`
  191. function. It takes care of updating: the position (if the move is accepted),
  192. the stepsize (to track a given target acceptance rate) and the average
  193. acceptance rate (computed as a moving average).
  194. Parameters
  195. ----------
  196. positions: shared variable, theano matrix
  197. Shared theano matrix whose rows contain the old position
  198. stepsize: shared variable, theano scalar
  199. Shared theano scalar containing current step size
  200. avg_acceptance_rate: shared variable, theano scalar
  201. Shared theano scalar containing the current average acceptance rate
  202. final_pos: shared variable, theano matrix
  203. Shared theano matrix whose rows contain the new position
  204. accept: theano scalar
  205. Boolean-type variable representing whether or not the proposed HMC move
  206. should be accepted or not.
  207. target_acceptance_rate: float
  208. The stepsize is modified in order to track this target acceptance rate.
  209. stepsize_inc: float
  210. Amount by which to increment stepsize when acceptance rate is too high.
  211. stepsize_dec: float
  212. Amount by which to decrement stepsize when acceptance rate is too low.
  213. stepsize_min: float
  214. Lower-bound on `stepsize`.
  215. stepsize_min: float
  216. Upper-bound on `stepsize`.
  217. avg_acceptance_slowness: float
  218. Average acceptance rate is computed as an exponential moving average.
  219. (1-avg_acceptance_slowness) is the weight given to the newest
  220. observation.
  221. Returns
  222. -------
  223. rval1: dictionary-like
  224. A dictionary of updates to be used by the `HMC_Sampler.simulate`
  225. function. The updates target the position, stepsize and average
  226. acceptance rate.
  227. """
  228. ## POSITION UPDATES ##
  229. # broadcast `accept` scalar to tensor with the same dimensions as final_pos.
  230. accept_matrix = accept.dimshuffle(0, *(('x',)*(final_pos.ndim-1)))
  231. # if accept is True, update to `final_pos` else stay put
  232. new_positions = TT.switch(accept_matrix, final_pos, positions)
  233. ## STEPSIZE UPDATES ##
  234. # if acceptance rate is too low, our sampler is too "noisy" and we reduce
  235. # the stepsize. If it is too high, our sampler is too conservative, we can
  236. # get away with a larger stepsize (resulting in better mixing).
  237. _new_stepsize = TT.switch(avg_acceptance_rate > target_acceptance_rate,
  238. stepsize * stepsize_inc, stepsize * stepsize_dec)
  239. # maintain stepsize in [stepsize_min, stepsize_max]
  240. new_stepsize = TT.clip(_new_stepsize, stepsize_min, stepsize_max)
  241. ## ACCEPT RATE UPDATES ##
  242. # perform exponential moving average
  243. new_acceptance_rate = TT.add(
  244. avg_acceptance_slowness * avg_acceptance_rate,
  245. (1.0 - avg_acceptance_slowness) * accept.mean())
  246. return [(positions, new_positions),
  247. (stepsize, new_stepsize),
  248. (avg_acceptance_rate, new_acceptance_rate)]
  249. class HMC_sampler(object):
  250. """
  251. Convenience wrapper for performing Hybrid Monte Carlo (HMC). It creates the
  252. symbolic graph for performing an HMC simulation (using `hmc_move` and
  253. `hmc_updates`). The graph is then compiled into the `simulate` function, a
  254. theano function which runs the simulation and updates the required shared
  255. variables.
  256. Users should interface with the sampler thorugh the `draw` function which
  257. advances the markov chain and returns the current sample by calling
  258. `simulate` and `get_position` in sequence.
  259. The hyper-parameters are the same as those used by Marc'Aurelio's
  260. 'train_mcRBM.py' file (available on his personal home page).
  261. """
  262. def __init__(self, **kwargs):
  263. self.__dict__.update(kwargs)
  264. @classmethod
  265. def new_from_shared_positions(cls, shared_positions, energy_fn,
  266. initial_stepsize=0.01, target_acceptance_rate=.9, n_steps=20,
  267. stepsize_dec = 0.98,
  268. stepsize_min = 0.001,
  269. stepsize_max = 0.25,
  270. stepsize_inc = 1.02,
  271. avg_acceptance_slowness = 0.9, # used in geometric avg. 1.0 would be not moving at all
  272. seed=12345):
  273. """
  274. :param shared_positions: theano ndarray shared var with many particle [initial] positions
  275. :param energy_fn:
  276. callable such that energy_fn(positions)
  277. returns theano vector of energies.
  278. The len of this vector is the batchsize.
  279. The sum of this energy vector must be differentiable (with theano.tensor.grad) with
  280. respect to the positions for HMC sampling to work.
  281. """
  282. batchsize = shared_positions.shape[0]
  283. # allocate shared variables
  284. stepsize = sharedX(initial_stepsize, 'hmc_stepsize')
  285. avg_acceptance_rate = sharedX(target_acceptance_rate, 'avg_acceptance_rate')
  286. s_rng = TT.shared_randomstreams.RandomStreams(seed)
  287. # define graph for an `n_steps` HMC simulation
  288. accept, final_pos = hmc_move(
  289. s_rng,
  290. shared_positions,
  291. energy_fn,
  292. stepsize,
  293. n_steps)
  294. # define the dictionary of updates, to apply on every `simulate` call
  295. simulate_updates = hmc_updates(
  296. shared_positions,
  297. stepsize,
  298. avg_acceptance_rate,
  299. final_pos=final_pos,
  300. accept=accept,
  301. stepsize_min=stepsize_min,
  302. stepsize_max=stepsize_max,
  303. stepsize_inc=stepsize_inc,
  304. stepsize_dec=stepsize_dec,
  305. target_acceptance_rate=target_acceptance_rate,
  306. avg_acceptance_slowness=avg_acceptance_slowness)
  307. # compile theano function
  308. simulate = function([], [], updates=simulate_updates)
  309. # create HMC_sampler object with the following attributes ...
  310. return cls(
  311. positions=shared_positions,
  312. stepsize=stepsize,
  313. stepsize_min=stepsize_min,
  314. stepsize_max=stepsize_max,
  315. avg_acceptance_rate=avg_acceptance_rate,
  316. target_acceptance_rate=target_acceptance_rate,
  317. s_rng=s_rng,
  318. _updates=simulate_updates,
  319. simulate=simulate)
  320. def draw(self, **kwargs):
  321. """
  322. Returns a new position obtained after `n_steps` of HMC simulation.
  323. Parameters
  324. ----------
  325. kwargs: dictionary
  326. The `kwargs` dictionary is passed to the shared variable
  327. (self.positions) `get_value()` function. For example, to avoid
  328. copying the shared variable value, consider passing `borrow=True`.
  329. Returns
  330. -------
  331. rval: numpy matrix
  332. Numpy matrix whose of dimensions similar to `initial_position`.
  333. """
  334. self.simulate()
  335. return self.positions.get_value(borrow=False)