Использование машинного обучения для оценки проекта.
В одной из предыдущих статей мы создали статистическую модель для прогнозирования фактических сроков и стоимости проекта на основе оценок. Мы обсудили, что можем подогнать оценки (как для проектов Agile, так и для проектов Waterfall) к логнормальному распределению, которое гарантирует положительную поддержку. Использование статистического подхода к оценке позволяет нам давать прогнозы с требуемым уровнем достоверности, а также прогнозировать денежные выгоды, затраты и риски, как мы обсуждали в другом посте.
Меня спросили, как модель обобщается на случай, когда оценка дана в виде диапазона. В самом деле, это то, чему нас все учили: давать не одно число, а диапазон. Один из подходов - продолжать использовать нашу статистическую модель и вводить в нее число в середине, среднее из двух значений.
Таким образом, модель может использоваться без модификаций.
У этого подхода есть две проблемы:
- Среднее значение максимума и минимума является произвольным. Это уменьшает вдвое предоставляемую информацию. Было бы лучше, чтобы алгоритм изучил, где нам нужно установить переменную x в интервале между низкой и высокой границей.
- Предоставляя нам ряд данных, разработчик пытается донести до нас очень важную информацию: степень неопределенности оценок. Правильная модель должна использовать эту информацию.
Чтобы упростить процесс, мы возьмем натуральный логарифм всех оценок и фактических данных. Поскольку мы моделируем оценки с использованием логарифмически нормального распределения, наши новые переменные y
, l
, h
будут логарифмами фактического количества дней, нижних и верхних оценок соответственно. В этом случае мы можем использовать нормальное распределение! Мы будем моделировать y
, используя линейную регрессию:
В случае, когда θh и θl равны, мы получаем точно такую же проблему, как мы обсуждали ранее.
Функцию правдоподобия для отдельного фрагмента данных в этом случае можно записать следующим образом (после this).
Как упоминалось ранее, указав диапазон, разработчик хотел сообщить нам о неопределенности оценки. Мы должны включить эту неопределенность в нашу оценку σ. Интуитивно диапазон пропорционален стандартному отклонению, и мы можем узнать коэффициент, моделируя σ как:
Если мы также используем параметр точности τ вместо σ 0:
Тогда наша функция правдоподобия будет:
Априори для τ и θ традиционно являются гамма-распределением и нормальным распределением соответственно:
Здесь α, β, λ - гиперпараметры.
Выбор априора для ζ сложнее. Для выбранного нами вида функции правдоподобия не существует ни одного сопряженного априорного значения. Пока мы можем выбрать нормальное распределение. Нулевое среднее этого распределения означает, что мы априори не доверяем диапазонам (мы знаем, что у многих консультантов диапазон всегда составляет 20% и не передает никакой информации). Высокое среднее априорного распределения означает, что мы уделяем больше внимания предполагаемой степени неопределенности.
Для простоты мы установили среднее значение равным нулю.
Отрицательная лог-апостериорная функция:
В этом блоге я найду параметры, соответствующие максимальной апостериорной оценке. А чтобы не делать ошибок при различении, мы будем использовать TensorFlow. Мы будем следовать этому примеру, чтобы построить наш код.
import numpy as np import pandas as pd import tensorflow as tf
Данные здесь представляют собой приблизительное и фактическое количество дней. Мы видим, что разработчик любил добавлять 25% к своей оценке в качестве буфера, однако для некоторых историй он добавил больше буфера, возможно, для обозначения большей неопределенности.
seed=1389 tf.reset_default_graph() task_data = pd.DataFrame({'low':[4,14,4,3,4,3,4,9,6,27,20,23,11], 'high':[5,18,5,4,5,7,5,10,8,30,25,29,14], 'actual':[17,8,5,3,5,4,9,9,4,27,16,15,7,]}) %matplotlib inline import matplotlib.pyplot as plt import seaborn as sns fig, ax = plt.subplots(figsize=(11.7, 8.27)) task_data['story_id'] = task_data.index data_for_plot = pd.melt(task_data, id_vars="story_id", var_name="type", value_name="days") task_data.drop(columns=['story_id'], inplace=True) sns.barplot(x='story_id', y='days', hue='type', data=data_for_plot,ax=ax);
При определении переменных мы заменяем τ другой переменной ρ:
Это сделано для того, чтобы оптимизатор не выбрал отрицательное значение τ.
#Taking the log of data log_data = np.log(task_data.values) N = log_data.shape[0] #Defining variables theta_h = tf.Variable(name='theta_h', initial_value=0.5) theta_l = tf.Variable(name='theta_l', initial_value=0.5) zeta = tf.Variable(name='zeta', initial_value=0.01) rho = tf.Variable(name='rho', initial_value=0.01)
Поскольку мы не хотим настраивать слишком много гиперпараметров, мы установим α и β равными единице. Оба параметра λ действуют как параметры регуляризации, поэтому нам придется их настроить.
#Set the hyperparameters alpha = tf.constant(name='alpha', value=1.0) beta = tf.constant(name='beta', value=1.0) lambda1 = tf.constant(name='lambda1', value=1e-4) lambda2 = tf.constant(name='lambda2', value=1e-4) def loss(l, h, y): return tf.log(1+zeta**2*(h-l)) + \ rho**2/2/(1+zeta**2*(h-l))**2 * (y - theta_l*l - theta_h*h)**2 cummulative_loss = tf.reduce_sum(list(np.apply_along_axis(lambda x: loss(*x), axis=1, arr=log_data ))) cost = cummulative_loss - (N+1-2*alpha)/2*tf.log(rho**2) + beta*rho**2 + \ rho**2*lambda1/2*(theta_h**2+theta_l**2) + rho**2*lambda2/2*zeta**2 learning_rate = 1e-4 optimizer = tf.train.AdamOptimizer(learning_rate) train_op = optimizer.minimize(cost) import math init = tf.global_variables_initializer() n_epochs = int(1e5) with tf.Session() as sess: sess.run(init) for epoch in range(n_epochs): if epoch % 1e4 == 0: print("Epoch", epoch, "Cost =", cost.eval()) print(f'Parameters: {theta_l.eval()}, {theta_h.eval()}, {rho.eval()}, {zeta.eval()}') sess.run(train_op) best_theta_l = theta_l.eval() best_theta_h = theta_h.eval() best_sigma = 1/math.sqrt(rho.eval()) Epoch 0 Cost = 55.26268 Parameters: 0.5, 0.5, 0.009999999776482582, 0.009999999776482582 Epoch 10000 Cost = 6.5892615 Parameters: 0.24855799973011017, 0.6630115509033203, 0.6332486271858215, 1.1534561276317736e-35 Epoch 20000 Cost = 1.39517 Parameters: 0.2485545128583908, 0.6630078554153442, 1.3754394054412842, 1.1534561276317736e-35 Epoch 30000 Cost = 1.3396643 Parameters: 0.24855604767799377, 0.6630094647407532, 1.4745615720748901, 1.1534561276317736e-35 Epoch 40000 Cost = 1.3396641 Parameters: 0.24855272471904755, 0.6630063056945801, 1.4745622873306274, 1.1534561276317736e-35 Epoch 50000 Cost = 1.3396646 Parameters: 0.2485586702823639, 0.6630119681358337, 1.4745632410049438, 1.1534561276317736e-35 Epoch 60000 Cost = 1.3396648 Parameters: 0.2485581487417221, 0.6630115509033203, 1.4745649099349976, 1.1534561276317736e-35 Epoch 70000 Cost = 1.3396643 Parameters: 0.2485586702823639, 0.6630122065544128, 1.4745644330978394, 1.1534561276317736e-35 Epoch 80000 Cost = 1.3396643 Parameters: 0.24855820834636688, 0.6630116701126099, 1.4745631217956543, 1.1534561276317736e-35 Epoch 90000 Cost = 1.3396646 Parameters: 0.248562291264534, 0.663015604019165, 1.474563717842102, 1.1534561276317736e-35
Интересно то, что ζ равно нулю. Это означает, что мы не можем доверять оценке неопределенности, которую дают нам разработчики. Это также означает, что мы можем просто использовать логарифмически-нормальное распределение вокруг среднего, заданного изученными параметрами θl и θh. Допустим, тот же разработчик оценил выполнение новой задачи в 10–15 дней. Подставляя его в формулы, мы видим:
mu = best_theta_l*math.log(10)+best_theta_h*math.log(15) most_likely_prediction = math.exp(mu) most_likely_prediction 10.67385532327305
Мы также можем получить 95% -ную уверенность, подставив значения непосредственно в логнормальное распределение:
from scipy.stats import lognorm distribution = lognorm(s=best_sigma, scale=most_likely_prediction, loc=0) print(f'95% confidence: {distribution.ppf(0.95)}') 95% confidence: 41.3614192940211
Как мы видим, если мы хотим 95% уверенности, мы должны дать оценку 41 день вместо 11 дней для 50% уверенности. Это очень легко объяснить, если вы видите, что в прошлом разработчик не очень хорошо оценивал задачи.
Вы можете получить доступ к записной книжке с github.