| |
| |
| """ |
| ..author:: Alex Müller, ETH Zürich, Switzerland. |
| ..date:: September 2017 |
| |
| Code for training a LSTM model on peptide sequences followed by sampling novel sequences through the model. |
| Check the readme for possible flags to use with this script. |
| """ |
| import json |
| import os |
| import pickle |
| import random |
| import argparse |
|
|
| import matplotlib.pyplot as plt |
| import numpy as np |
| import tensorflow as tf |
|
|
| from tensorflow.keras.callbacks import ModelCheckpoint |
| from tensorflow.keras.initializers import RandomNormal |
| from tensorflow.keras.layers import Dense, LSTM, GRU |
| from tensorflow.keras.models import Sequential, load_model |
| |
| from tensorflow.keras.optimizers.legacy import Adam |
| from tensorflow.keras.regularizers import l2 |
| from modlamp.analysis import GlobalAnalysis |
| from modlamp.core import count_aas |
| from modlamp.descriptors import PeptideDescriptor, GlobalDescriptor |
| from modlamp.sequences import Random, Helices |
| from progressbar import ProgressBar |
| from scipy.spatial import distance |
| from sklearn.model_selection import KFold |
| from sklearn.preprocessing import StandardScaler |
|
|
| plt.switch_backend('agg') |
| flags = argparse.ArgumentParser() |
| flags.add_argument("-d", "--dataset", default="training_sequences_noC.csv", help="dataset file (expecting csv)", type=str) |
| flags.add_argument("-n", "--name", default="test", help="run name for log and checkpoint files", type=str) |
| flags.add_argument("-b", "--batch_size", default=128, help="batch size", type=int) |
| flags.add_argument("-e", "--epochs", default=50, help="epochs to train", type=int) |
| flags.add_argument("-l", "--layers", default=2, help="number of layers in the network", type=int) |
| flags.add_argument("-x", "--neurons", default=256, help="number of units per layer", type=int) |
| flags.add_argument("-c", "--cell", default="LSTM", help="type of neuron to use, available: LSTM, GRU", type=str) |
| flags.add_argument("-o", "--dropout", default=0.1, help="dropout to use in every layer; layer 1 gets 1*dropout, layer 2 2*dropout etc.", type=float) |
| |
| flags.add_argument("-t", "--train", default=True, help="whether the network should be trained or just sampled from", type=str) |
| flags.add_argument("-v", "--valsplit", default=0.2, help="fraction of the data to use for validation", type=float) |
| flags.add_argument("-s", "--sample", default=100, help="number of sequences to sample training", type=int) |
| flags.add_argument("-p", "--temp", default=1.25, help="temperature used for sampling", type=float) |
| flags.add_argument("-m", "--maxlen", default=0, help="maximum sequence length allowed when sampling new sequences", type=int) |
| flags.add_argument("-f", "--fminlen", default=7, help="minimum sequence length allowed when sampling new sequences", type=int) |
| flags.add_argument("-a", "--startchar", default="B", help="starting character to begin sampling. Default='B' for 'begin'", type=str) |
| flags.add_argument("-r", "--lr", default=0.01, help="learning rate to be used with the Adam optimizer", type=float) |
| flags.add_argument("--l2", default=None, help="l2 regularization rate. If None, no l2 regularization is used", type=float) |
| flags.add_argument("--modfile", default=None, help="filename of the pretrained model to used for sampling if train=False", type=str) |
| |
| flags.add_argument("--finetune", default=False, help="if True, a pretrained model provided in modfile is finetuned on the dataset", type=str) |
| flags.add_argument("--cv", default=None, help="number of folds to use for cross-validation; if None, no CV is performed", type=int) |
| flags.add_argument("--window", default=0, help="window size used to process sequences. If 0, all sequences are padded to the longest sequence length in the dataset", type=int) |
| flags.add_argument("--step", default=1, help="step size to move window or prediction target", type=int) |
| flags.add_argument("--target", default="all", help="whether to learn all proceeding characters or just the last `one` in sequence", type=str) |
| flags.add_argument("--padlen", default=0, help="number of spaces to use for padding sequences (if window not 0); if 0, sequences are padded to the length of the longest sequence in the dataset", type=int) |
| |
| flags.add_argument("--refs", default=True, help="whether reference sequence sets should be generated for the analysis", type=str) |
| args = flags.parse_args() |
|
|
|
|
| def _save_flags(filename): |
| """ Function to save used arguments to log-file |
| |
| :return: saved file |
| """ |
| with open(filename, 'w') as f: |
| f.write("Used flags:\n-----------\n") |
| json.dump(args.__dict__, f, indent=2) |
|
|
|
|
| def _onehotencode(s, vocab=None): |
| """ Function to one-hot encode a sring. |
| |
| :param s: {str} String to encode in one-hot fashion |
| :param vocab: vocabulary to use fore encoding, if None, default AAs are used |
| :return: one-hot encoded string as a np.array |
| """ |
| if not vocab: |
| vocab = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', |
| 'Y', ' '] |
| |
| |
| to_one_hot = dict() |
| for i, a in enumerate(vocab): |
| v = np.zeros(len(vocab)) |
| v[i] = 1 |
| to_one_hot[a] = v |
| |
| result = [] |
| for l in s: |
| result.append(to_one_hot[l]) |
| result = np.array(result) |
| return np.reshape(result, (1, result.shape[0], result.shape[1])), to_one_hot, vocab |
|
|
|
|
| def _onehotdecode(matrix, vocab=None, filename=None): |
| """ Decode a given one-hot represented matrix back into sequences |
| |
| :param matrix: matrix containing sequence patterns that are one-hot encoded |
| :param vocab: vocabulary, if None, standard AAs are used |
| :param filename: filename for saving sequences, if ``None``, sequences are returned in a list |
| :return: list of decoded sequences in the range lenmin-lenmax, if ``filename``, they are saved to a file |
| """ |
| if not vocab: |
| _, _, vocab = _onehotencode('A') |
| if len(matrix.shape) == 2: |
| result = [] |
| for i in range(matrix.shape[0]): |
| for j in range(matrix.shape[1]): |
| aa = np.where(matrix[i, j] == 1.)[0][0] |
| result.append(vocab[aa]) |
| seq = ''.join(result) |
| if filename: |
| with open(filename, 'wb') as f: |
| f.write(seq) |
| else: |
| return seq |
| |
| elif len(matrix.shape) == 3: |
| result = [] |
| for n in range(matrix.shape[0]): |
| oneresult = [] |
| for i in range(matrix.shape[1]): |
| for j in range(matrix.shape[2]): |
| aa = np.where(matrix[n, i, j] == 1.)[0][0] |
| oneresult.append(vocab[aa]) |
| seq = ''.join(oneresult) |
| result.append(seq) |
| if filename: |
| with open(filename, 'wb') as f: |
| for s in result: |
| f.write(s + '\n') |
| else: |
| return result |
|
|
|
|
| def _sample_with_temp(preds, temp=1.0): |
| """ Helper function to sample one letter from a probability array given a temperature. |
| |
| :param preds: {np.array} predictions returned by the network |
| :param temp: {float} temperature value to sample at. |
| """ |
| streched = np.log(preds) / temp |
| stretched_probs = np.exp(streched) / np.sum(np.exp(streched)) |
| return np.random.choice(len(streched), p=stretched_probs) |
|
|
|
|
| def load_model_instance(filename): |
| """ Load a whole Model class instance from a given epoch file |
| |
| :param filename: epoch file, e.g. model_epoch_5.hdf5 |
| :return: model instance with trained weights |
| """ |
| modfile = os.path.dirname(filename) + '/model.p' |
| mod = pickle.load(open(modfile, 'rb')) |
| hdf5_file = ''.join(modfile.split('.')[:-1]) + '.hdf5' |
| mod.model = load_model(hdf5_file) |
| return mod |
|
|
|
|
| def save_model_instance(mod): |
| """ Save a whole Model instance and the corresponding model with weights to two files (model.p and model.hdf5) |
| |
| :param mod: model instance |
| :return: saved model files in the checkpoint dir |
| """ |
| tmp = mod.model |
| tmp.save(mod.checkpointdir + 'model.hdf5') |
| mod.model = None |
| pickle.dump(mod, open(mod.checkpointdir + 'model.p', 'wb')) |
| mod.model = tmp |
|
|
|
|
| class SequenceHandler(object): |
| """ Class for handling peptide sequences, e.g. loading, one-hot encoding or decoding and saving """ |
| |
| def __init__(self, window=0, step=2, refs=True): |
| """ |
| :param window: {str} window used for chopping up sequences. If 0: False |
| :param step: {int} size of the steps to move the window forward |
| :param refs {bool} whether to generate reference sequence sets for analysis |
| """ |
| self.sequences = None |
| self.generated = None |
| self.ran = None |
| self.hel = None |
| self.X = list() |
| self.y = list() |
| self.window = window |
| self.step = step |
| self.refs = refs |
| |
| _, self.to_one_hot, self.vocab = _onehotencode('A') |
| |
| def load_sequences(self, filename): |
| """ Method to load peptide sequences from a csv file |
| |
| :param filename: {str} filename of the sequence file to be read (``csv``, one sequence per line) |
| :return: sequences in self.sequences |
| """ |
| with open(filename) as f: |
| self.sequences = [s.strip() for s in f] |
| self.sequences = random.sample(self.sequences, len(self.sequences)) |
| |
| def pad_sequences(self, pad_char=' ', padlen=0): |
| """ Pad all sequences to the longest length (default, padlen=0) or a given length |
| |
| :param pad_char: {str} Character to pad sequences with |
| :param padlen: {int} Custom length of padding to add to all sequences to (optional), default: 0. If |
| 0, sequences are padded to the length of the longest sequence in the training set. If a window is used and the |
| padded sequence is shorter than the window size, it is padded to fit the window. |
| """ |
| if padlen: |
| padded_seqs = [] |
| for seq in self.sequences: |
| if len(seq) < self.window: |
| padded_seq = seq + pad_char * (self.step + self.window - len(seq)) |
| else: |
| padded_seq = seq + pad_char * padlen |
| padded_seqs.append(padded_seq) |
| else: |
| length = max([len(seq) for seq in self.sequences]) |
| padded_seqs = [] |
| for seq in self.sequences: |
| padded_seq = 'B' + seq + pad_char * (length - len(seq)) |
| padded_seqs.append(padded_seq) |
| |
| if pad_char not in self.vocab: |
| self.vocab += [pad_char] |
| |
| self.sequences = padded_seqs |
| |
| def one_hot_encode(self, target='all'): |
| """ Chop up loaded sequences into patterns of length ``window`` by moving by stepsize ``step`` and translate |
| them with a one-hot vector encoding |
| |
| :param target: {str} whether all proceeding AA should be learned or just the last one in sequence (`all`, `one`) |
| :return: one-hot encoded sequence patterns in self.X and corresponding target amino acids in self.y |
| """ |
| if self.window == 0: |
| for s in self.sequences: |
| self.X.append([self.to_one_hot[char] for char in s[:-self.step]]) |
| if target == 'all': |
| self.y.append([self.to_one_hot[char] for char in s[self.step:]]) |
| elif target == 'one': |
| self.y.append(s[-self.step:]) |
| |
| self.X = np.reshape(self.X, (len(self.X), len(self.sequences[0]) - self.step, len(self.vocab))) |
| self.y = np.reshape(self.y, (len(self.y), len(self.sequences[0]) - self.step, len(self.vocab))) |
| |
| else: |
| for s in self.sequences: |
| for i in range(0, len(s) - self.window, self.step): |
| self.X.append([self.to_one_hot[char] for char in s[i: i + self.window]]) |
| if target == 'all': |
| self.y.append([self.to_one_hot[char] for char in s[i + 1: i + self.window + 1]]) |
| elif target == 'one': |
| self.y.append(s[-self.step:]) |
| |
| self.X = np.reshape(self.X, (len(self.X), self.window, len(self.vocab))) |
| self.y = np.reshape(self.y, (len(self.y), self.window, len(self.vocab))) |
| |
| print("\nData shape:\nX: " + str(self.X.shape) + "\ny: " + str(self.y.shape)) |
| |
| def analyze_training(self): |
| """ Method to analyze the distribution of the training data |
| |
| :return: prints out information about the length distribution of the sequences in ``self.sequences`` |
| """ |
| d = GlobalDescriptor(self.sequences) |
| d.length() |
| print("\nLENGTH DISTRIBUTION OF TRAINING DATA:\n") |
| print("Number of sequences: \t%i" % len(self.sequences)) |
| print("Mean sequence length: \t%.1f ± %.1f" % (np.mean(d.descriptor), np.std(d.descriptor))) |
| print("Median sequence length: \t%i" % np.median(d.descriptor)) |
| print("Minimal sequence length:\t%i" % np.min(d.descriptor)) |
| print("Maximal sequence length:\t%i" % np.max(d.descriptor)) |
| |
| def analyze_generated(self, num, fname='analysis.txt', plot=False): |
| """ Method to analyze the generated sequences located in `self.generated`. |
| |
| :param num: {int} wanted number of sequences to sample |
| :param fname: {str} filename to save analysis info to |
| :param plot: {bool} whether to plot an overview of descriptors |
| :return: file with analysis info (distances) |
| """ |
| with open(fname, 'w') as f: |
| print("Analyzing...") |
| f.write("ANALYSIS OF SAMPLED SEQUENCES\n==============================\n\n") |
| f.write("Nr. of duplicates in generated sequences: %i\n" % (len(self.generated) - len(set(self.generated)))) |
| count = len(set(self.generated) & set(self.sequences)) |
| f.write("%.1f percent of generated sequences are present in the training data.\n" % |
| ((count / len(self.generated)) * 100)) |
| d = GlobalDescriptor(self.generated) |
| len1 = len(d.sequences) |
| d.filter_aa('B') |
| len2 = len(d.sequences) |
| d.length() |
| f.write("\n\nLENGTH DISTRIBUTION OF GENERATED DATA:\n\n") |
| f.write("Number of sequences too short:\t%i\n" % (num - len1)) |
| f.write("Number of invalid (with 'B'):\t%i\n" % (len1 - len2)) |
| f.write("Number of valid unique seqs:\t%i\n" % len2) |
| f.write("Mean sequence length: \t\t%.1f ± %.1f\n" % (np.mean(d.descriptor), np.std(d.descriptor))) |
| f.write("Median sequence length: \t\t%i\n" % np.median(d.descriptor)) |
| f.write("Minimal sequence length: \t\t%i\n" % np.min(d.descriptor)) |
| f.write("Maximal sequence length: \t\t%i\n" % np.max(d.descriptor)) |
| |
| descriptor = 'pepcats' |
| seq_desc = PeptideDescriptor([s[1:].rstrip() for s in self.sequences], descriptor) |
| seq_desc.calculate_autocorr(7) |
| gen_desc = PeptideDescriptor(d.sequences, descriptor) |
| gen_desc.calculate_autocorr(7) |
| |
| |
| self.ran = Random(len(self.generated), np.min(d.descriptor), np.max(d.descriptor)) |
| probas = count_aas(''.join(seq_desc.sequences)).values() |
| self.ran.generate_sequences(proba=probas) |
| ran_desc = PeptideDescriptor(self.ran.sequences, descriptor) |
| ran_desc.calculate_autocorr(7) |
| |
| |
| self.hel = Helices(len(self.generated), np.min(d.descriptor), np.max(d.descriptor)) |
| self.hel.generate_sequences() |
| hel_desc = PeptideDescriptor(self.hel.sequences, descriptor) |
| hel_desc.calculate_autocorr(7) |
| |
| |
| f.write("\n\nDISTANCE CALCULATION IN '%s' DESCRIPTOR SPACE\n\n" % descriptor.upper()) |
| desc_dist = distance.cdist(gen_desc.descriptor, seq_desc.descriptor, metric='euclidean') |
| f.write("Average euclidean distance of sampled to training data:\t%.3f +/- %.3f\n" % |
| (np.mean(desc_dist), np.std(desc_dist))) |
| ran_dist = distance.cdist(ran_desc.descriptor, seq_desc.descriptor, metric='euclidean') |
| f.write("Average euclidean distance if randomly sampled seqs:\t%.3f +/- %.3f\n" % |
| (np.mean(ran_dist), np.std(ran_dist))) |
| hel_dist = distance.cdist(hel_desc.descriptor, seq_desc.descriptor, metric='euclidean') |
| f.write("Average euclidean distance if amphipathic helical seqs:\t%.3f +/- %.3f\n" % |
| (np.mean(hel_dist), np.std(hel_dist))) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| def save_generated(self, logdir, filename): |
| """ Save all sequences in `self.generated` to file |
| |
| :param logdir: {str} current log directory (used for comparison sequences) |
| :param filename: {str} filename to save the sequences to |
| :return: saved file |
| """ |
| with open(filename, 'w') as f: |
| for s in self.generated: |
| f.write(s + '\n') |
| |
| |
| |
|
|
|
|
| class Model(object): |
| """ |
| Class containing the LSTM model to learn sequential data |
| """ |
| |
| def __init__(self, n_vocab, outshape, session_name, cell="LSTM", n_units=256, batch=64, layers=2, lr=0.001, |
| dropoutfract=0.1, loss='categorical_crossentropy', l2_reg=None, ask=True, seed=42): |
| """ Initialize the model |
| |
| :param n_vocab: {int} length of vocabulary |
| :param outshape: {int} output dimensionality of the model |
| :param session_name: {str} custom name for the current session. Will create directory with this name to save |
| results / logs to. |
| :param n_units: {int} number of LSTM units per layer |
| :param batch: {int} batch size |
| :param layers: {int} number of layers in the network |
| :param loss: {str} applied loss function, choose from available keras loss functions |
| :param lr: {float} learning rate to use with Adam optimizer |
| :param dropoutfract: {float} fraction of dropout to add to each layer. Layer1 gets 1 * value, Layer2 2 * |
| value and so on. |
| :param l2_reg: {float} l2 regularization for kernel |
| :param seed {int} random seed used to initialize weights |
| """ |
| random.seed(seed) |
| self.seed = seed |
| self.dropout = dropoutfract |
| self.inshape = (None, n_vocab) |
| self.outshape = outshape |
| self.neurons = n_units |
| self.layers = layers |
| self.losses = list() |
| self.val_losses = list() |
| self.batchsize = batch |
| self.lr = lr |
| self.cv_loss = None |
| self.cv_loss_std = None |
| self.cv_val_loss = None |
| self.cv_val_loss_std = None |
| self.model = None |
| self.cell = cell |
| self.losstype = loss |
| self.session_name = session_name |
| self.logdir = './' + session_name |
| self.l2 = l2_reg |
| if ask and os.path.exists(self.logdir): |
| decision = input('\nSession folder already exists!\n' |
| 'Do you want to overwrite the previous session? [y/n] ') |
| if decision in ['n', 'no', 'N', 'NO', 'No']: |
| self.logdir = './' + input('Enter new session name: ') |
| os.makedirs(self.logdir) |
| self.checkpointdir = self.logdir + '/checkpoint/' |
| if not os.path.exists(self.checkpointdir): |
| os.makedirs(self.checkpointdir) |
| _, _, self.vocab = _onehotencode('A') |
| |
| self.initialize_model(seed=self.seed) |
| |
| def initialize_model(self, seed=42): |
| """ Method to initialize the model with all parameters saved in the attributes. This method is used during |
| initialization of the class, as well as in cross-validation to reinitialize a fresh model for every fold. |
| |
| :param seed: {int} random seed to use for weight initialization |
| |
| :return: initialized model in ``self.model`` |
| """ |
| self.losses = list() |
| self.val_losses = list() |
| self.cv_loss = None |
| self.cv_loss_std = None |
| self.cv_val_loss = None |
| self.cv_val_loss_std = None |
| self.model = None |
| weight_init = RandomNormal(mean=0.0, stddev=0.05, seed=seed) |
| optimizer = Adam(lr=self.lr, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0) |
| |
| if self.l2: |
| l2reg = l2(self.l2) |
| else: |
| l2reg = None |
| |
| self.model = Sequential() |
| for l in range(self.layers): |
| if self.cell == "GRU": |
| self.model.add(GRU(units=self.neurons, |
| name='GRU%i' % (l + 1), |
| input_shape=self.inshape, |
| return_sequences=True, |
| kernel_initializer=weight_init, |
| kernel_regularizer=l2reg, |
| dropout=self.dropout * (l + 1))) |
| else: |
| self.model.add(LSTM(units=self.neurons, |
| name='LSTM%i' % (l + 1), |
| input_shape=self.inshape, |
| return_sequences=True, |
| kernel_initializer=weight_init, |
| kernel_regularizer=l2reg, |
| dropout=self.dropout * (l + 1), |
| recurrent_dropout=self.dropout * (l + 1))) |
| self.model.add(Dense(self.outshape, |
| name='Dense', |
| activation='softmax', |
| kernel_regularizer=self.l2, |
| kernel_initializer=weight_init)) |
| self.model.compile(loss=self.losstype, optimizer=optimizer) |
| with open(self.checkpointdir + "model.json", 'w') as f: |
| json.dump(self.model.to_json(), f) |
| self.model.summary() |
| |
| def finetuneinit(self, session_name): |
| """ Method to generate a new directory for finetuning a pre-existing model on a new dataset with a new name |
| |
| :param session_name: {str} new session name for finetuning |
| :return: generates all necessary session folders |
| """ |
| self.session_name = session_name |
| self.logdir = './' + session_name |
| if os.path.exists(self.logdir): |
| decision = input('\nSession folder already exists!\n' |
| 'Do you want to overwrite the previous session? [y/n] ') |
| if decision in ['n', 'no', 'N', 'NO', 'No']: |
| self.logdir = './' + input('Enter new session name: ') |
| os.makedirs(self.logdir) |
| self.checkpointdir = self.logdir + '/checkpoint/' |
| if not os.path.exists(self.checkpointdir): |
| os.makedirs(self.checkpointdir) |
| |
| def train(self, x, y, epochs=100, valsplit=0.2, sample=100): |
| """ Train the model on given training data. |
| |
| :param x: {array} training data |
| :param y: {array} targets for training data in X |
| :param epochs: {int} number of epochs to train |
| :param valsplit: {float} fraction of data that should be used as validation data during training |
| :param sample: {int} number of sequences to sample after every training epoch |
| :return: trained model and measured losses in self.model, self.losses and self.val_losses |
| """ |
| writer = tf.summary.create_file_writer('./logs/' + self.session_name) |
| with writer.as_default(): |
| for e in range(epochs): |
| print("Epoch %i" % e) |
| checkpoints = [ModelCheckpoint(filepath=self.checkpointdir + 'model_epoch_%i.hdf5' % e, verbose=0)] |
| train_history = self.model.fit(x, y, epochs=1, batch_size=self.batchsize, validation_split=valsplit, |
| shuffle=False, callbacks=checkpoints) |
| tf.summary.scalar('loss', train_history.history['loss'][-1], step=e) |
| self.losses.append(train_history.history['loss']) |
| if valsplit > 0.: |
| self.val_losses.append(train_history.history['val_loss']) |
| tf.summary.scalar('val_loss', train_history.history['val_loss'][-1], step=e) |
| if sample: |
| for s in self.sample(sample): |
| print(s) |
| writer.close() |
|
|
| def cross_val(self, x, y, epochs=100, cv=5, plot=True): |
| """ Method to perform cross-validation with the model given data X, y |
| |
| :param x: {array} training data |
| :param y: {array} targets for training data in X |
| :param epochs: {int} number of epochs to train |
| :param cv: {int} fold |
| :param plot: {bool} whether the losses should be plotted and saved to the session folder |
| :return: |
| """ |
| self.losses = list() |
| self.val_losses = list() |
| kf = KFold(n_splits=cv) |
| cntr = 0 |
| for train, test in kf.split(x): |
| print("\nFold %i" % (cntr + 1)) |
| self.initialize_model(seed=cntr) |
| train_history = self.model.fit(x[train], y[train], epochs=epochs, batch_size=self.batchsize, |
| validation_data=(x[test], y[test])) |
| self.losses.append(train_history.history['loss']) |
| self.val_losses.append(train_history.history['val_loss']) |
| cntr += 1 |
| self.cv_loss = np.mean(self.losses, axis=0) |
| self.cv_loss_std = np.std(self.losses, axis=0) |
| self.cv_val_loss = np.mean(self.val_losses, axis=0) |
| self.cv_val_loss_std = np.std(self.val_losses, axis=0) |
| if plot: |
| self.plot_losses(cv=True) |
| |
| |
| minloss = np.min(self.cv_val_loss) |
| e = np.where(minloss == self.cv_val_loss)[0][0] |
| print("\n%i-fold cross-validation result:\n\nBest epoch:\t%i\nVal_loss:\t%.4f" % (cv, e, minloss)) |
| with open(self.logdir + '/' + self.session_name + '_best_epoch.txt', 'w') as f: |
| f.write("%i-fold cross-validation result:\n\nBest epoch:\t%i\nVal_loss:\t%.4f" % (cv, e, minloss)) |
| |
| def plot_losses(self, show=False, cv=False): |
| """Plot the losses obtained in training. |
| |
| :param show: {bool} Whether the plot should be shown or saved. If ``False``, the plot is saved to the |
| session folder. |
| :param cv: {bool} Whether the losses from cross-validation should be plotted. The standard deviation will be |
| depicted as filled areas around the mean curve. |
| :return: plot (saved) or shown interactive |
| """ |
| fig, ax = plt.subplots() |
| ax.set_title('LSTM Categorical Crossentropy Loss Plot', fontweight='bold', fontsize=16) |
| if cv: |
| filename = self.logdir + '/' + self.session_name + '_cv_loss_plot.pdf' |
| x = range(1, len(self.cv_loss) + 1) |
| ax.plot(x, self.cv_loss, '-', color='#FE4365', label='Training') |
| ax.plot(x, self.cv_val_loss, '-', color='k', label='Validation') |
| ax.fill_between(x, self.cv_loss + self.cv_loss_std, self.cv_loss - self.cv_loss_std, |
| facecolors='#FE4365', alpha=0.5) |
| ax.fill_between(x, self.cv_val_loss + self.cv_val_loss_std, self.cv_val_loss - self.cv_val_loss_std, |
| facecolors='k', alpha=0.5) |
| ax.set_xlim([0.5, len(self.cv_loss) + 0.5]) |
| minloss = np.min(self.cv_val_loss) |
| plt.text(x=0.5, y=0.5, s='best epoch: ' + str(np.where(minloss == self.cv_val_loss)[0][0]) + ', val_loss: ' |
| + str(minloss.round(4)), transform=ax.transAxes) |
| else: |
| filename = self.logdir + '/' + self.session_name + '_loss_plot.pdf' |
| x = range(1, len(self.losses) + 1) |
| ax.plot(x, self.losses, '-', color='#FE4365', label='Training') |
| if self.val_losses: |
| ax.plot(x, self.val_losses, '-', color='k', label='Validation') |
| ax.set_xlim([0.5, len(self.losses) + 0.5]) |
| ax.set_ylabel('Loss', fontweight='bold', fontsize=14) |
| ax.set_xlabel('Epoch', fontweight='bold', fontsize=14) |
| ax.spines['right'].set_visible(False) |
| ax.spines['top'].set_visible(False) |
| ax.xaxis.set_ticks_position('bottom') |
| ax.yaxis.set_ticks_position('left') |
| plt.legend(loc='best') |
| if show: |
| plt.show() |
| else: |
| plt.savefig(filename) |
| |
| def sample(self, num=100, minlen=7, maxlen=50, start=None, temp=2.5, show=False): |
| """Invoke generation of sequence patterns through sampling from the trained model. |
| |
| :param num: {int} number of sequences to sample |
| :param minlen {int} minimal allowed sequence length |
| :param maxlen: {int} maximal length of each pattern generated, if 0, a random length is chosen between 7 and 50 |
| :param start: {str} start AA to be used for sampling. If ``None``, a random AA is chosen |
| :param temp: {float} temperature value to sample at. |
| :param show: {bool} whether the sampled sequences should be printed out |
| :return: {array} matrix of patterns of shape (num, seqlen, inputshape[0]) |
| """ |
| print("\nSampling...\n") |
| sampled = [] |
| lcntr = 0 |
| pbar = ProgressBar() |
| for rs in pbar(range(num)): |
| random.seed(rs) |
| if not maxlen: |
| longest = np.random.randint(7, 50) |
| else: |
| longest = maxlen |
| |
| if start: |
| start_aa = start |
| else: |
| start_aa = 'B' |
| sequence = start_aa |
| |
| while sequence[-1] != ' ' and len(sequence) <= longest: |
| x, _, _ = _onehotencode(sequence) |
| preds = self.model.predict(x)[0][-1] |
| next_aa = _sample_with_temp(preds, temp=temp) |
| sequence += self.vocab[next_aa] |
| |
| if start_aa == 'B': |
| sequence = sequence[1:].rstrip() |
| else: |
| sequence = sequence.rstrip() |
| |
| if len(sequence) < minlen: |
| lcntr += 1 |
| continue |
| |
| sampled.append(sequence) |
| if show: |
| print(sequence) |
| |
| print("\t%i sequences were shorter than %i" % (lcntr, minlen)) |
| return sampled |
|
|
| def load_model(self, filename): |
| """Method to load a trained model from a hdf5 file |
| |
| :return: model loaded from file in ``self.model`` |
| """ |
| self.model.load_weights(filename) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| def main(infile, sessname, neurons=64, layers=2, epochs=100, batchsize=128, window=0, step=1, target='all', |
| valsplit=0.2, sample=100, aa='B', temperature=2.5, cell="LSTM", dropout=0.1, train=True, learningrate=0.01, |
| modfile=None, samplelength=36, minlen=7, pad=0, l2_rate=None, cv=None, finetune=False, references=True): |
| |
| data = SequenceHandler(window=window, step=step, refs=references) |
| print("Loading sequences...") |
| data.load_sequences(infile) |
| data.analyze_training() |
| |
| |
| print("\nPadding sequences...") |
| data.pad_sequences(padlen=pad) |
| |
| |
| print("One-hot encoding sequences...") |
| data.one_hot_encode(target=target) |
|
|
| if train == True: |
| |
| print("\nBuilding model...") |
| model = Model(n_vocab=len(data.vocab), outshape=len(data.vocab), session_name=sessname, n_units=neurons, |
| batch=batchsize, layers=layers, cell=cell, loss='categorical_crossentropy', lr=learningrate, |
| dropoutfract=dropout, l2_reg=l2_rate, ask=True, seed=42) |
| print("Model built!") |
| |
| if cv: |
| print("\nPERFORMING %i-FOLD CROSS-VALIDATION...\n" % cv) |
| model.cross_val(data.X, data.y, epochs=epochs, cv=cv) |
| model.initialize_model(seed=42) |
| model.train(data.X, data.y, epochs=epochs, valsplit=0.0, sample=0) |
| model.plot_losses() |
| else: |
| |
| print("\nTRAINING MODEL FOR %i EPOCHS...\n" % epochs) |
| model.train(data.X, data.y, epochs=epochs, valsplit=valsplit, sample=0) |
| model.plot_losses() |
| |
| save_model_instance(model) |
| |
| elif finetune: |
| print("\nUSING PRETRAINED MODEL FOR FINETUNING... (%s)\n" % modfile) |
| print("Loading model...") |
| model = load_model_instance(modfile) |
| model.load_model(modfile) |
| model.finetuneinit(sessname) |
| print("Finetuning model...") |
| model.train(data.X, data.y, epochs=epochs, valsplit=valsplit, sample=0) |
| model.plot_losses() |
| save_model_instance(model) |
| else: |
| print("\nUSING PRETRAINED MODEL... (%s)\n" % modfile) |
| model = load_model_instance(modfile) |
| model.load_model(modfile) |
| |
| print(model.model.summary()) |
| |
| |
| print("\nSAMPLING %i SEQUENCES...\n" % sample) |
| data.generated = model.sample(sample, start=aa, maxlen=samplelength, minlen=minlen, show=False, temp=temperature) |
| |
| data.save_generated(model.logdir, model.logdir + '/sampled_sequences_temp' + str(temperature) + '.csv') |
|
|
|
|
| if __name__ == "__main__": |
| |
| main(infile=args.dataset, sessname=args.name, batchsize=args.batch_size, epochs=args.epochs, |
| layers=args.layers, valsplit=args.valsplit, neurons=args.neurons, cell=args.cell, sample=args.sample, |
| temperature=args.temp, dropout=args.dropout, train=args.train, modfile=args.modfile, |
| learningrate=args.lr, cv=args.cv, samplelength=args.maxlen, minlen=args.fminlen, window=args.window, |
| step=args.step, aa=args.startchar, l2_rate=args.l2, target=args.target, pad=args.padlen, |
| finetune=args.finetune, references=args.refs) |
| |
| |
| _save_flags("./" + args.name + "/flags.txt") |
|
|