Last active
April 27, 2018 10:17
-
-
Save tteofili/97b3196d1dc3e54e933a42cc69dfea8f to your computer and use it in GitHub Desktop.
min char/word-level vanilla RNN (Java, nd4j, comons-math3)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Apache License 2.0 | |
| * see https://www.apache.org/licenses/LICENSE-2.0 | |
| */ | |
| import org.apache.commons.math3.distribution.EnumeratedDistribution; | |
| import org.apache.commons.math3.util.Pair; | |
| import org.nd4j.linalg.api.iter.NdIndexIterator; | |
| import org.nd4j.linalg.api.ndarray.INDArray; | |
| import org.nd4j.linalg.api.ops.impl.transforms.SetRange; | |
| import org.nd4j.linalg.api.ops.impl.transforms.SoftMax; | |
| import org.nd4j.linalg.factory.Nd4j; | |
| import org.nd4j.linalg.ops.transforms.Transforms; | |
| import java.io.IOException; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.LinkedList; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| /** | |
| * A min char/word-level vanilla RNN model, based on Andrej Karpathy's python code. | |
| * See also: | |
| * | |
| * @see <a href="http://karpathy.github.io/2015/05/21/rnn-effectiveness">The Unreasonable Effectiveness of Recurrent Neural Networks</a> | |
| * @see <a href="https://gist.github.com/karpathy/d4dee566867f8291f086">Minimal character-level language model with a Vanilla Recurrent Neural Network, in Python/numpy</a> | |
| */ | |
| public class RNN { | |
| // hyperparameters | |
| protected final float learningRate; // size of hidden layer of neurons | |
| protected final int seqLength; // no. of steps to unroll the RNN for | |
| protected final int hiddenLayerSize; | |
| protected final int epochs; | |
| protected final boolean useChars; | |
| protected final int vocabSize; | |
| protected final Map<String, Integer> charToIx; | |
| protected final Map<Integer, String> ixToChar; | |
| protected final List<String> data; | |
| private final static double reg = 1e-8; | |
| // model parameters | |
| private final INDArray wxh; // input to hidden | |
| private final INDArray whh; // hidden to hidden | |
| private final INDArray why; // hidden to output | |
| private final INDArray bh; // hidden bias | |
| private final INDArray by; // output bias | |
| private INDArray hPrev = null; // memory state | |
| public RNN(float learningRate, int seqLength, int hiddenLayerSize, int epochs, String text) { | |
| this(learningRate, seqLength, hiddenLayerSize, epochs, text, true); | |
| } | |
| public RNN(float learningRate, int seqLength, int hiddenLayerSize, int epochs, String text, boolean useChars) { | |
| this.learningRate = learningRate; | |
| this.seqLength = seqLength; | |
| this.hiddenLayerSize = hiddenLayerSize; | |
| this.epochs = epochs; | |
| this.useChars = useChars; | |
| String[] textTokens = useChars ? toStrings(text.toCharArray()) : text.split(" "); | |
| data = new LinkedList<>(); | |
| Collections.addAll(data, textTokens); | |
| Set<String> tokens = new HashSet<>(data); | |
| vocabSize = tokens.size(); | |
| System.out.printf("data has %d tokens, %d unique.\n", data.size(), vocabSize); | |
| charToIx = new HashMap<>(); | |
| ixToChar = new HashMap<>(); | |
| int i = 0; | |
| for (String c : tokens) { | |
| charToIx.put(c, i); | |
| ixToChar.put(i, c); | |
| i++; | |
| } | |
| wxh = Nd4j.randn(hiddenLayerSize, vocabSize).mul(0.01); | |
| whh = Nd4j.randn(hiddenLayerSize, hiddenLayerSize).mul(0.01); | |
| why = Nd4j.randn(vocabSize, hiddenLayerSize).mul(0.01); | |
| bh = Nd4j.zeros(hiddenLayerSize, 1); | |
| by = Nd4j.zeros(vocabSize, 1); | |
| } | |
| private String[] toStrings(char[] chars) { | |
| String[] strings = new String[chars.length]; | |
| for (int i = 0; i < chars.length; i++) { | |
| strings[i] = String.valueOf(chars[i]); | |
| } | |
| return strings; | |
| } | |
| public void learn() { | |
| int currentEpoch = 0; | |
| int n = 0; | |
| int p = 0; | |
| // memory variables for Adagrad | |
| INDArray mWxh = Nd4j.zerosLike(wxh); | |
| INDArray mWhh = Nd4j.zerosLike(whh); | |
| INDArray mWhy = Nd4j.zerosLike(why); | |
| INDArray mbh = Nd4j.zerosLike(bh); | |
| INDArray mby = Nd4j.zerosLike(by); | |
| // loss at iteration 0 | |
| double smoothLoss = -Math.log(1.0 / vocabSize) * seqLength; | |
| while (true) { | |
| // prepare inputs (we're sweeping from left to right in steps seqLength long) | |
| if (p + seqLength + 1 >= data.size() || n == 0) { | |
| hPrev = Nd4j.zeros(hiddenLayerSize, 1); // reset RNN memory | |
| p = 0; // go from start of data | |
| currentEpoch++; | |
| if (currentEpoch == epochs) { | |
| System.out.println("training finished: e:" + epochs + ", l: " + smoothLoss + ", h:(" + learningRate + ", " + seqLength + ", " + hiddenLayerSize + ")"); | |
| break; | |
| } | |
| } | |
| INDArray inputs = getSequence(p); | |
| INDArray targets = getSequence(p + 1); | |
| // sample from the model every now and then | |
| if (n % 100 == 0 && n > 0) { | |
| String txt = sample(inputs.getInt(0)); | |
| System.out.printf("\n---\n %s \n----\n", txt); | |
| } | |
| INDArray dWxh = Nd4j.zerosLike(wxh); | |
| INDArray dWhh = Nd4j.zerosLike(whh); | |
| INDArray dWhy = Nd4j.zerosLike(why); | |
| INDArray dbh = Nd4j.zerosLike(bh); | |
| INDArray dby = Nd4j.zerosLike(by); | |
| // forward seqLength characters through the net and fetch gradient | |
| double loss = lossFun(inputs, targets, dWxh, dWhh, dWhy, dbh, dby); | |
| smoothLoss = smoothLoss * 0.999 + loss * 0.001; | |
| if (Double.isNaN(smoothLoss)) { | |
| System.out.println("loss is NaN (over/underflow occured, try adjusting hyperparameters)"); | |
| break; | |
| } | |
| if (n % 100 == 0) { | |
| System.out.printf("iter %d, loss: %f\n", n, smoothLoss); // print progress | |
| } | |
| // perform parameter update with Adagrad | |
| mWxh.addi(dWxh.mul(dWxh)); | |
| wxh.subi((dWxh.mul(learningRate)).div(Transforms.sqrt(mWxh.add(reg)))); | |
| mWhh.addi(dWhh.mul(dWhh)); | |
| whh.subi(dWhh.mul(learningRate).div(Transforms.sqrt(mWhh.add(reg)))); | |
| mWhy.addi(dWhy.mul(dWhy)); | |
| why.subi(dWhy.mul(learningRate).div(Transforms.sqrt(mWhy.add(reg)))); | |
| mbh.addi(dbh.mul(dbh)); | |
| bh.subi(dbh.mul(learningRate).div(Transforms.sqrt(mbh.add(reg)))); | |
| mby.addi(dby.mul(dby)); | |
| by.subi(dby.mul(learningRate).div(Transforms.sqrt(mby.add(reg)))); | |
| p += seqLength; // move data pointer | |
| n++; // iteration counter | |
| } | |
| } | |
| protected INDArray getSequence(int p) { | |
| INDArray inputs = Nd4j.create(seqLength); | |
| int c = 0; | |
| for (String ch : data.subList(p, p + seqLength)) { | |
| Integer ix = charToIx.get(ch); | |
| inputs.putScalar(c, ix); | |
| c++; | |
| } | |
| return inputs; | |
| } | |
| /** | |
| * inputs, targets are both list of integers | |
| * hprev is Hx1 array of initial hidden state | |
| * returns the modified loss, gradients on model parameters | |
| */ | |
| private double lossFun(INDArray inputs, INDArray targets, INDArray dWxh, INDArray dWhh, INDArray dWhy, INDArray dbh, | |
| INDArray dby) { | |
| INDArray xs = Nd4j.zeros(inputs.length(), vocabSize); | |
| INDArray hs = null; | |
| INDArray ys = null; | |
| INDArray ps = null; | |
| INDArray hs1 = Nd4j.create(hPrev.shape()); | |
| Nd4j.copy(hPrev, hs1); | |
| double loss = 0; | |
| // forward pass | |
| for (int t = 0; t < inputs.length(); t++) { | |
| int tIndex = inputs.getScalar(t).getInt(0); | |
| xs.putScalar(t, tIndex, 1); // encode in 1-of-k representation | |
| INDArray hsRow = t == 0 ? hs1 : hs.getRow(t - 1); | |
| INDArray hst = Transforms.tanh((wxh.mmul(xs.getRow(t).transpose())).add((whh.mmul(hsRow)).add(bh))); // hidden state | |
| if (hs == null) { | |
| hs = init(inputs.length(), hst); | |
| } | |
| hs.putRow(t, hst); | |
| INDArray yst = (why.mmul(hst)).add(by); // unnormalized log probabilities for next chars | |
| if (ys == null) { | |
| ys = init(inputs.length(), yst); | |
| } | |
| ys.putRow(t, yst); | |
| INDArray pst = Nd4j.getExecutioner().execAndReturn(new SoftMax(yst)); // probabilities for next chars | |
| if (ps == null) { | |
| ps = init(inputs.length(), pst); | |
| } | |
| ps.putRow(t, pst); | |
| loss += -Math.log(pst.getDouble(targets.getInt(t))); // softmax (cross-entropy loss) | |
| } | |
| this.hPrev = hs.getRow(inputs.length() - 1); | |
| // backward pass: compute gradients going backwards | |
| INDArray dhNext = Nd4j.zerosLike(hs.getRow(0)); | |
| for (int t = inputs.length() - 1; t >= 0; t--) { | |
| INDArray dy = ps.getRow(t); | |
| dy.putRow(targets.getInt(t), dy.getRow(targets.getInt(t)).sub(1)); // backprop into y | |
| INDArray hst = hs.getRow(t); | |
| dWhy.addi(dy.mmul(hst.transpose())); // derivative of hy layer | |
| dby.addi(dy); | |
| INDArray dh = why.transpose().mmul(dy).add(dhNext); // backprop into h | |
| INDArray dhraw = (Nd4j.ones(hst.shape()).sub(hst.mul(hst))).mul(dh); // backprop through tanh nonlinearity | |
| dbh.addi(dhraw); | |
| dWxh.addi(dhraw.mmul(xs.getRow(t))); | |
| INDArray hsRow = t == 0 ? hs1 : hs.getRow(t - 1); | |
| dWhh.addi(dhraw.mmul(hsRow.transpose())); | |
| dhNext = whh.transpose().mmul(dhraw); | |
| } | |
| // clip exploding gradients | |
| Nd4j.getExecutioner().exec(new SetRange(dWxh, -5, 5)); | |
| Nd4j.getExecutioner().exec(new SetRange(dWhh, -5, 5)); | |
| Nd4j.getExecutioner().exec(new SetRange(dWhy, -5, 5)); | |
| Nd4j.getExecutioner().exec(new SetRange(dbh, -5, 5)); | |
| Nd4j.getExecutioner().exec(new SetRange(dby, -5, 5)); | |
| return loss; | |
| } | |
| protected INDArray init(int t, INDArray ast) { | |
| INDArray as; | |
| int[] aShape = ast.shape(); | |
| int[] shape = new int[1 + aShape.length]; | |
| shape[0] = t; | |
| System.arraycopy(aShape, 0, shape, 1, aShape.length); | |
| as = Nd4j.create(shape); | |
| return as; | |
| } | |
| /** | |
| * sample a sequence of integers from the model, using current (hPrev) memory state, seedIx is seed letter for first time step | |
| */ | |
| public String sample(int seedIx) { | |
| INDArray x = Nd4j.zeros(vocabSize, 1); | |
| x.putScalar(seedIx, 1); | |
| int sampleSize = 2 * seqLength; | |
| INDArray ixes = Nd4j.create(sampleSize); | |
| for (int t = 0; t < sampleSize; t++) { | |
| hPrev = Transforms.tanh((wxh.mmul(x)).add((whh.mmul(hPrev)).add(bh))); | |
| INDArray y = (why.mmul(hPrev)).add(by); | |
| INDArray pm = Nd4j.getExecutioner().execAndReturn(new SoftMax(y)).ravel(); | |
| List<Pair<Integer, Double>> d = new LinkedList<>(); | |
| for (int pi = 0; pi < vocabSize; pi++) { | |
| d.add(new Pair<>(pi, pm.getDouble(0, pi))); | |
| } | |
| EnumeratedDistribution<Integer> distribution = new EnumeratedDistribution<>(d); | |
| int ix = distribution.sample(); | |
| x = Nd4j.zeros(vocabSize, 1); | |
| x.putScalar(ix, 1); | |
| ixes.putScalar(t, ix); | |
| } | |
| return getSampleString(ixes); | |
| } | |
| protected String getSampleString(INDArray ixes) { | |
| StringBuilder txt = new StringBuilder(); | |
| NdIndexIterator ndIndexIterator = new NdIndexIterator(ixes.shape()); | |
| while (ndIndexIterator.hasNext()) { | |
| int[] next = ndIndexIterator.next(); | |
| if (!useChars && txt.length() > 0) { | |
| txt.append(' '); | |
| } | |
| txt.append(ixToChar.get(ixes.getInt(next))); | |
| } | |
| return txt.toString(); | |
| } | |
| @Override | |
| public String toString() { | |
| return getClass().getName() + "{" + | |
| "learningRate=" + learningRate + | |
| ", seqLength=" + seqLength + | |
| ", hiddenLayerSize=" + hiddenLayerSize + | |
| ", epochs=" + epochs + | |
| ", vocabSize=" + vocabSize + | |
| ", useChars=" + useChars + | |
| '}'; | |
| } | |
| public String getHyperparamsString() { | |
| return getClass().getName() + "{" + | |
| "wxh=" + wxh + | |
| ", whh=" + whh + | |
| ", why=" + why + | |
| ", bh=" + bh + | |
| ", by=" + by + | |
| '}'; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment