blob: fe0eaa9856a65383c34d42a97314bd0892c29393 [file] [log] [blame]
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tensorflow models used in Bad CL Detector service."""
from __future__ import print_function
import functools
import tensorflow as tf
from lib import feature_preprocessor
# pylint: disable=pointless-statement
# pylint: disable=no-value-for-parameter
# a small number to add to prediction result for taking log.
LOG_RESIDUAL = 1e-15
def DoubleWrap(function):
"""A decorator decorator.
This allows decorator to be used without parentheses if no arguments are
provided. All arguments must be optional.
ref: https://danijar.com/structuring-your-tensorflow-models/
Args:
function: a function to wrap.
Returns:
a decorator to wrap functions.
"""
@functools.wraps(function)
def Decorator(*args, **kwargs):
if (len(args) == 1) and (len(kwargs) is 0) and (callable(args[0])):
return function(args[0])
else:
return lambda wrapee: function(wrapee, *args, **kwargs)
return Decorator
@DoubleWrap
def DefineScope(function, scope=None, *args, **kwargs):
"""A decorator for functions that define TensorFlow operations.
The wrapped function will only be executed once. Subsequent calls to it will
directly return the result so that operations are added to the graph only
once. The operations added by the function live within a tf.variable_scope().
If this decorator is used with arguments, they will be forwarded to the
variable scope. The scope name defaults to the name of the wrapped function.
ref: https://danijar.com/structuring-your-tensorflow-models/
Args:
function: a tensorflow operation function to wrap.
scope: string, the name of the scope of variables in the function.
*args: optional arguments to pass to function.
**kwargs: optional arguments to pass to function.
Returns:
a decorator to wrap tensorflow operation function.
"""
attribute = '_cache_' + function.__name__
name = scope or function.__name__
@property
@functools.wraps(function)
def Decorator(self):
if not hasattr(self, attribute):
with tf.variable_scope(name, *args, **kwargs):
setattr(self, attribute, function(self))
return getattr(self, attribute)
return Decorator
class BadCLExistenceModel(object):
"""Model to calculate probability of having a bad cl for a failed build."""
# size of first hidden layer.
HIDDEN_LAYER_1_SIZE = 16
# size of second hidden layer.
HIDDEN_LAYER_2_SIZE = 8
# number of output classes, should be 2 for 'bad_cl' and 'non bad_cl'.
OUTPUT_CLASSES = 2
# learning rate for ML pipeline.
LEARNING_RATE = 0.0001
L2_LOSS_COEFFICIENT = 0.01
def __init__(self, features, labels, keep_prob):
"""Initialize a FailureTypeModel.
Args:
features: stage features as produced by StageFeaturePreprocessor.
labels: stage labels as produced by StageFeaturePreprocessor.
keep_prob: a float of (0, 1]. Keep probability for the dropout layer.
"""
self.features = features
self.labels = labels
self.keep_prob = keep_prob
# these statements are necessary in tensorflow model. Refer to ref in
# define_scope()
self.Predict
self.Optimize
self.GetError
@DefineScope(initializer=tf.contrib.slim.xavier_initializer())
def Predict(self):
"""Calculates the probabilities of CQ failure reasons being bad cl.
Returns:
a numpy array of floats in range [0, 1]. Each float represents the
probabilities of the build failure reason is 'bad cl'.
"""
nodes = self.features
nodes = tf.contrib.slim.fully_connected(nodes, self.HIDDEN_LAYER_1_SIZE)
nodes = tf.contrib.slim.dropout(nodes, self.keep_prob)
nodes = tf.contrib.slim.fully_connected(nodes, self.HIDDEN_LAYER_2_SIZE)
nodes = tf.contrib.slim.dropout(nodes, self.keep_prob)
probabilities = tf.contrib.slim.fully_connected(nodes,
self.OUTPUT_CLASSES,
tf.nn.softmax)
return probabilities
@DefineScope
def Optimize(self):
"""Returns a tensflow optimizer operation function.
Returns:
an optimizer for tensflow model.
"""
# weights and biases in the graph defined above. Elements 0, 2, 4 are
# weights while 1, 3, 5 are biases. Apply L2 regularization to weights.
weights_biases = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES)
one_hot = tf.one_hot(self.labels, self.OUTPUT_CLASSES)
loss = (tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
logits=self.Predict, labels=one_hot)) +
self.L2_LOSS_COEFFICIENT*tf.nn.l2_loss(weights_biases[0]) +
self.L2_LOSS_COEFFICIENT*tf.nn.l2_loss(weights_biases[2]) +
self.L2_LOSS_COEFFICIENT*tf.nn.l2_loss(weights_biases[4]))
optimizer = tf.train.RMSPropOptimizer(self.LEARNING_RATE)
return optimizer.minimize(loss)
@DefineScope
def GetError(self):
"""Calculates the training error in current model.
Returns:
a float in range [0, 1], which is the percentage of wrongly classified
build counts over total build counts in the CQ.
"""
one_hot = tf.one_hot(self.labels, 2)
mistakes = tf.not_equal(tf.argmax(one_hot, 1),
tf.argmax(self.Predict, 1))
error = tf.reduce_mean(tf.cast(mistakes, tf.float32), name='error')
return error
class BadCLDetectionModel(object):
"""Model to calculate probability of a CL to be a bad one."""
LEARNING_RATE = 0.005
L2_LOSS_COEFFICIENT = 0.05
# total class count: bad cl or other cl
OUTPUT_CLASSES = 2
# hidden layer 1 size
HIDDEN_LAYER_1_SIZE = 16
# hidden layer 2 size
HIDDEN_LAYER_2_SIZE = 8
# parameters for RNN of exception features
EXCEPTION_EMBEDDING_SIZE = 100
EXCEPTION_OUTPUT_SIZE = 32
EXCEPTION_LSTM_SIZE = 64
EXCEPTION_LSTM_LAYERS = 1
# parameters for RNN of commit features
COMMIT_EMBEDDING_SIZE = 200
COMMIT_OUTPUT_SIZE = 64
COMMIT_LSTM_SIZE = 128
COMMIT_LSTM_LAYERS = 1
# RNN vocabulary sizes for exception and commit, plus one for None input.
EXCEPTION_RNN_SIZE = len(
feature_preprocessor.CLFeaturePreprocessor.GetWord2IntMapping(
None, 'exception_rnn', False)) + 1
COMMIT_RNN_SIZE = len(
feature_preprocessor.CLFeaturePreprocessor.GetWord2IntMapping(
None, 'commit_rnn', False)) + 1
def __init__(self, nn_features, exception_rnn_input, commit_rnn_input, labels,
keep_prob):
self.nn_features = nn_features
self.exception_rnn_input = exception_rnn_input
self.commit_rnn_input = commit_rnn_input
self.labels = labels
self.keep_prob = keep_prob
self.Predict
self.Optimize
self.GetError
@DefineScope(initializer=tf.contrib.slim.xavier_initializer())
def Predict(self):
"""Calculates the probabilities of CLs being bad.
Returns:
a numpy array of floats in range [0, 1]. Each float represents the chance
of a CL being bad.
"""
with tf.variable_scope('exception_rnn'):
exception_lstm_cell = self.GetLSTMCellStack(self.EXCEPTION_LSTM_SIZE,
self.EXCEPTION_LSTM_LAYERS)
exception_rnn_input = self.GetEmbeddedInput(self.exception_rnn_input,
self.EXCEPTION_RNN_SIZE,
self.EXCEPTION_EMBEDDING_SIZE)
exception_rnn_outputs = self.GetRNNOutputs(exception_lstm_cell,
exception_rnn_input,
self.EXCEPTION_OUTPUT_SIZE)
with tf.variable_scope('commit_rnn'):
commit_lstm_cell = self.GetLSTMCellStack(self.COMMIT_LSTM_SIZE,
self.COMMIT_LSTM_LAYERS)
commit_rnn_input = self.GetEmbeddedInput(self.commit_rnn_input,
self.COMMIT_RNN_SIZE,
self.COMMIT_EMBEDDING_SIZE)
commit_rnn_outputs = self.GetRNNOutputs(commit_lstm_cell,
commit_rnn_input,
self.COMMIT_OUTPUT_SIZE)
rnn_outputs = tf.concat([exception_rnn_outputs, commit_rnn_outputs], 1)
# get final output from mixure NN model.
logits = self.GetNNOuputs(rnn_outputs)
return logits
def GetLSTMCellStack(self, cell_size, number_of_layers):
"""Get a lstm cell stack with dropout layers.
Args:
cell_size: an int, size of the LSTM cell.
number_of_layers: an int, number of LSTM cell layers.
Returns:
A tuple of (stacked LSTM cells).
"""
lstm_cell = tf.contrib.rnn.BasicLSTMCell(cell_size)
drop = tf.contrib.rnn.DropoutWrapper(lstm_cell,
output_keep_prob=self.keep_prob)
# Stack up multiple LSTM cells for deep learning
lstm_cell_stack = tf.contrib.rnn.MultiRNNCell([drop] * number_of_layers)
return lstm_cell_stack
def GetRNNOutputs(self, lstm_cell, embedded_input, output_size):
"""Get RNN features for given embedded input and LSTM cell.
Args:
lstm_cell: a stack of BasicLSTMCell.
embedded_input: a numpy array of embedded text inputs.
output_size: an int, number of output nodes of current RNN.
Returns:
fully connected layer of size output_size from RNN.
"""
raw_outputs, _ = tf.nn.dynamic_rnn(lstm_cell, embedded_input,
dtype=tf.float32)
# connect the last layer of RNN to RNN_OUT_SIZE output nodes.
rnn_outputs = tf.contrib.layers.fully_connected(raw_outputs[:, -1],
output_size,
activation_fn=tf.sigmoid)
return rnn_outputs
def GetNNOuputs(self, rnn_outputs):
"""Get output logics from the mixture regular and recurrent NN.
Args:
rnn_outputs: output features from the RNNs.
Returns:
final logits of classification results.
"""
weights, biases = self.GetWeightsAndBiases(
rnn_outputs.get_shape().as_list()[1])
# Combine regular nn features and RNN outputs
layer_1_inputs = tf.concat([self.nn_features, rnn_outputs], 1)
# First hidden layer with RELU activation
layer_1 = tf.add(tf.matmul(layer_1_inputs, weights['hidden_layer_1']),
biases['hidden_layer_1'])
layer_1 = tf.nn.relu(layer_1)
# add a dropout layer
layer_1 = tf.nn.dropout(layer_1, self.keep_prob)
# add a second layer
layer_2 = tf.add(tf.matmul(layer_1, weights['hidden_layer_2']),
biases['hidden_layer_2'])
# Combine RNN and NN layers to produce output logits
logits = tf.add(tf.matmul(layer_2, weights['output']), biases['output'])
return logits
def GetEmbeddedInput(self, rnn_input, vocabulary_size, embedding_size):
"""Get embedded input of the text ids as inputs for RNN.
Args:
rnn_input: a numpy array of word id integers.
vocabulary_size: an integer, total size of vaocabulary of given input.
embedding_size: an integer, output embedding dimension of input.
Returns:
an embedded input ready for RNN.
"""
embedding = tf.Variable(tf.random_uniform((vocabulary_size,
embedding_size), -1, 1))
return tf.nn.embedding_lookup(embedding, rnn_input)
def GetWeightsAndBiases(self, rnn_outputs_dim):
"""Get weights and biases.
Args:
rnn_outputs_dim: an integer, the dimension of all RNN outputs.
Returns:
weights and biases dictionaries, to be used in the NN.
"""
# regular features dimension + RNN output dimension
input_layer_size = (self.nn_features.get_shape().as_list()[1] +
rnn_outputs_dim)
with tf.variable_scope('defining_weights'):
layer_1_weights = tf.get_variable(
'layer_1_weights', [input_layer_size, self.HIDDEN_LAYER_1_SIZE])
layer_2_weights = tf.get_variable(
'layer_2_weights', [self.HIDDEN_LAYER_1_SIZE,
self.HIDDEN_LAYER_2_SIZE])
out_weights = tf.get_variable(
'out_weights', [self.HIDDEN_LAYER_2_SIZE, self.OUTPUT_CLASSES])
weights = {
'hidden_layer_1': layer_1_weights,
'hidden_layer_2': layer_2_weights,
'output': out_weights}
biases = {
'hidden_layer_1': tf.Variable(tf.random_normal(
[self.HIDDEN_LAYER_1_SIZE])),
'hidden_layer_2': tf.Variable(tf.random_normal(
[self.HIDDEN_LAYER_2_SIZE])),
'output': tf.Variable(tf.random_normal([self.OUTPUT_CLASSES]))}
return weights, biases
@DefineScope('Predict')
def Optimize(self):
"""Returns a tensflow optimizer operation function.
Returns:
an optimizer for tensflow model.
"""
with tf.variable_scope('defining_weights', reuse=True):
layer_1_weights = tf.get_variable('layer_1_weights')
layer_2_weights = tf.get_variable('layer_2_weights')
out_weights = tf.get_variable('out_weights')
one_hot = tf.one_hot(self.labels, self.OUTPUT_CLASSES)
loss = (tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
logits=self.Predict, labels=one_hot)) +
self.L2_LOSS_COEFFICIENT*tf.nn.l2_loss(layer_1_weights) +
self.L2_LOSS_COEFFICIENT*tf.nn.l2_loss(layer_2_weights) +
self.L2_LOSS_COEFFICIENT*tf.nn.l2_loss(out_weights))
optimizer = tf.train.RMSPropOptimizer(self.LEARNING_RATE)
return optimizer.minimize(loss)
@DefineScope
def GetError(self):
"""Calculates the training error in current model.
Returns:
a float in range [0, 1], which is the percentage of wrongly classified CL
counts over total CL counts.
"""
one_hot = tf.one_hot(self.labels, self.OUTPUT_CLASSES)
mistakes = tf.not_equal(tf.argmax(one_hot, 1),
tf.argmax(self.Predict, 1))
return tf.reduce_mean(tf.cast(mistakes, tf.float32))