Обучаем LSTM-сеть программировать
Давайте посмотрим, на что способны LSTM-сети. Опираясь на потрясающую статью Андрея Карпаты (Andrej Karpathy), мы исследуем LSTM-модели символьного уровня, которые обучаются на последовательностях символов и способны предсказывать следующий символ.
Несмотря на то, что они могут показаться немного игрушечными, модели символьного уровня бывают очень полезны и применяются даже поверх моделей уровня слов. Например:
-
• Представьте себе интеллектуальный инструмент для автодополнения кода. LSTM-модель (теоретически) может отслеживать возвращаемый тип метода, в котором мы находимся, и эффективно подсказывать, что следует вернуть. Кроме того, если мы возвращаем неправильный тип, она способна выявить ошибку без выполнения компиляции.
-
• Приложения в области обработки естественного языка, такие как машинный перевод, часто испытывают трудности с редкими словами. Как перевести слово, которое мы никогда не встречали раньше, или преобразовать прилагательное в наречие? Даже если мы знаем смысл твита, как сгенерировать новый хештег для этого твита? Модели символьного уровня способны «придумывать» новые слова, и эти способности открывают еще одну область интересных приложений.
Приступим. Для начала я развернул инстанс p2.xlarge на сервисе EC2 и обучил 3-слойную LSTM-сеть, используя в качестве обучающих данных исходные коды Apache Commons Lang. Ниже представлена программа, которую сгенерировала обученная модель:
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the «License»); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an «AS IS» BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.math4.linear;
import java.text.NumberFormat;
import java.io.ByteArrayInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math4.optim.nonlinear.scalar.GoalType;
import org.apache.commons.math4.ml.neuralnet.sofm.NeuronSquareMesh2D;
import org.apache.commons.math4.distribution.DescriptiveStatistics;
import org.apache.commons.math4.optim.nonlinear.scalar.NodeFieldIntegrator;
import org.apache.commons.math4.optim.nonlinear.scalar.GradientFunction;
import org.apache.commons.math4.optim.PointValuePair;
import org.apache.commons.numbers.core.Precision;
/**
* Natural infinite is defined in basic eigenvalues of a transform are in a subconsider for the optimization ties.
*
* This implementation is the computation at a collection of a set of the solvers.
*
* This class is returned the default precision parameters after a new value for the interpolation interpolators for barycenter.
*
* The distribution values do not ratio example function containing this interface, which should be used in uniform real distributions.
*
* This class generates a new standard deviation of the following conventions, the variance was reached as
* constructor, and invoke the interpolation arrays
*
* the same special corresponding to a representation.
*
*
* @since 1.2
*/
public class SinoutionIntegrator implements Serializable {
/** Serializable version identifier */
private static final long serialVersionUID = -7989543519820244888L;
/**
* Start distance between the instance and a result (does not all lead to the number of seconds).
*
* Note that this implementation this can prevent the permutation of the preneved statistics.
*
*
* Preconditions:
*
* if it is null, {@code null}. It does not dofine the base number.
*
* @param source the number of left size of the specified value
* @param numberOfPoints number of points to be checked
* @return the parameters for a public function.
*/
public static double fitness(final double[] sample) {
double additionalComputed = Double.POSITIVE_INFINITY;
for (int i = 1; i < dim; i++) {
final double coefficients[i] = point[i] * coefficients[i];
double diff = a * FastMath.cos(point[i]);
final double sum = FastMath.max(random.nextDouble(), alpha);
final double sum = FastMath.sin(optimal[i].getReal() — cholenghat);
final double lower = gamma * cHessian;
final double fs = factor * maxIterationCount;
if (temp > numberOfPoints — 1) {
final int pma = points.size();
boolean partial = points.toString();
final double segments = new double[2];
final double sign = pti * x2;
double n = 0;
for (int i = 0; i < n; i++) {
final double ds = normalizedState(i, k, difference * factor);
final double inv = alpha + temp;
final double rsigx = FastMath.sqrt(max);
return new String(degree, e);
}
}
// Perform the number to the function parameters from one count of the values
final PointValuePair part = new PointValuePair[n];
for (int i = 0; i < n; i++) {
if (i == 1) {
numberOfPoints = 1;
}
final double dev = FastMath.log(perturb(g, norm), values[i]);
if (Double.isNaN(y) &&
NaN) {
sum /= samples.length;
}
double i = 1;
for (int i = 0; i < n; i++) {
statistics[i] = FastMath.abs(point[i].sign() + rhs[i]);
}
return new PointValuePair(true, params);
}
}
}
/**
* Computes the number of values
* @throws NotPositiveException if {@code NumberIsTooSmallException if {@code seed <= 0}.
* @throws NullArgumentException if row or successes is null
*/
public static double numericalMean(double value) {
if (variance == null) {
throw new NotStrictlyPositiveException(LocalizedFormats.NUMBER_OF_SUBCORSE_TRANSTOR_POPULATIONS_COEFFICIENTS,
p, numberOfSuccesses, true);
}
return sum;
}
/**
* {@inheritDoc}
*/
@Override
public LeastSquaresProblem create(final StatisticalSummary sampleStats1,
final double[] values, final double alpha) throws MathIllegalArgumentException {
final double sum = sumLogImpl.toSubSpace(sample);
final double relativeAccuracy = getSumOfLogs();
final double[] sample1 = new double[dimension];
for (int i = 0; i < result.length; i++) {
verifyInterval.solve(params, alpha);
}
return max;
}
/**
* Test creates a new PolynomialFunction function
* @see #applyTo(double)
*/
@Test
public void testCosise() {
final double p = 7.7;
final double expected = 0.0;
final SearchInterval d = new Power(1.0, 0.0);
final double penalty = 1e-03;
final double init = 0.245;
final double t = 0.2;
final double result = (x + 1.0) / 2.0;
final double numeratorAdd = 13;
final double bhigh = 2 * (k — 1) * Math.acos();
Assert.assertEquals(0.0, true);
Assert.assertTrue(percentile.evaluate(singletonArray), 0);
Assert.assertEquals( 0.0, getNumberOfTrials(0, 0), 1E-10);
Assert.assertEquals(0.201949230731, percentile.evaluate(specialValues), 1.0e-3);
Assert.assertEquals(-10.0, distribution.inverseCumulativeProbability(0.50), 0);
Assert.assertEquals(0.0, solver.solve(100, f, 1.0, 0.5), 1.0e-10);
}
Код, безусловно, не идеален, но он лучше, чем код многих data scientist’ов, которых я знаю. Мы видим, что LSTM-сеть обучилась многим интересным (и правильным!) приемам программирования:
-
• Модель знает структуру классов: сверху лицензия, затем пакеты и импорт, далее комментарии и определение класса, и, наконец, переменные и методы. Модель также знает структуру методов: комментарии правильно организованы (описание, затем @param, затем @return и т.д.); декораторы размещены в нужных местах; методы, не являющиеся void, заканчиваются соответствующим оператором return. Следует особо отметить, что это поведение наблюдается на протяжении больших фрагментов кода. Посмотрите на размер фрагмента, представленного выше!
-
• Модель способна отслеживать подпрограммы и уровни вложенности: отступы всегда правильные; операторы условий (if) и циклов (for) всегда закрыты.
-
• Модель даже умеет создавать тесты.
Как ей это удается? Чтобы ответить на этот вопрос, давайте посмотрим на скрытые состояния (hidden state).
На рисунке ниже представлен нейрон, который, вероятно, отслеживает уровень отступа.
По мере продвижения по последовательности нейроны LSTM-сети активируются с различной интенсивностью. На рисунке представлен один конкретный нейрон. Каждая строка – это последовательность символов. Цвет клеток соответствует уровню активации: темно-синий цвет соответствует большим положительным величинам активации, темно-красный цвет соответствует большим отрицательным величинам активации.
На следующем рисунке представлен нейрон, подсчитывающий количество пробелов между табуляциями:
Ради интереса давайте посмотрим на вывод другой 3-слойной LSTM-сети, обученной на исходных кодах TensorFlow:
«»»Tests for softplus layer tests.»»»
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import collections
import numpy as np
from tensorflow.python.platform import test
class InvalidAllOpCost(Experiment):
def _runTestToIndForDead(self):
return self._divs()
def testPad(self):
with ops.Graph().as_default():
var = sess.run(bucketized_op)
self.assertAllClose(
list(variables.global_variables()), status.eval())
def testHttptimenaterRoutingOptimizerSize(self):
with self.test_session() as sess:
table = lookup_ops.IdTableWithHashBuckets(
keys=[‘id’, ‘z’],
example_id_column=’price’,
num_outputs=6,
input_columns=[‘dummy_range’, ‘feature’, ‘dimensions’])
with self.assertRaisesRegexp(ValueError, ‘Expected dict of rank dimensions’):
fc.numeric_column(‘aaa’, indices=[[0, 0], [1, 0]], dENGINE=dtypes.int64)
output = table.lookup(input_string)
# all input tensors in SparseColumn has dimensions [end_back_prob, dimension] in the format.
with self.assertRaisesRegexp(
TypeError, «Shape of values must be specified during training.»):
fc.bucketized_column(attrs, boundaries=[62, 62])
В интернете можно найти множество других интересных примеров, взгляните на них, если захотите.
Исследуем внутренние механизмы LSTM-сетей
Давайте займемся более глубоким анализом. В предыдущем разделе мы рассмотрели примеры скрытого состояния, но для нас большой интерес также представляет состояние ячейки (cell state) и другие механизмы памяти. Соответствует ли логика их активации нашим предположениям, или мы увидим неожиданные закономерности?
Счет
В качестве первого эксперимента давайте обучим LSTM-сеть считать. Вспомните, как модели из предыдущего раздела научились делать правильные отступы в коде на Java и Python!
Для нашего эксперимента я сгенерировал последовательности следующего вида:
aaaaaXbbbbb
Формула последовательности: N символов «a», затем разделитель «X», затем N символов «b», где 1 <= N <= 10.
На данных последовательностях я обучил 1-слойную LSTM-сеть с 10 скрытыми нейронами. Как и ожидалось, модель показывает прекрасные результаты в пределах обучающего диапазона, и даже способна обобщать на несколько шагов за его пределы. Модель начинает допускать ошибки, когда мы просим ее сосчитать до 19: получая на входе 19 символов «a», на выходе она дает лишь 18 символов «b».
aaaaaaaaaaaaaaaXbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaXbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaXbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaXbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaXbbbbbbbbbbbbbbbbbb # Here it begins to fail: the model is given 19 «a»s, but outputs only 18 «b»s.
Мы предполагаем, что существует скрытый нейрон, подсчитывающий количество символов «a». Наше предположение оправдывается:
Я создал небольшое веб-приложение для исследования LSTM-сетей. Похоже, нейрон #2 подсчитывает как количество символов «a», так и количество символов «b». Напомню, что величина активации соответствует цвету клеток от темно-красного (-1) до темно-синего (+1).
Теперь посмотрим на состояние ячейки. Здесь мы наблюдаем аналогичную ситуацию:
Следует отметить одну интересную особенность: рабочая память является более «контрастной» версией долговременной памяти. Интересно, наблюдается ли это явление в отношении других нейронов?
Оказывается, да. На самом деле, такое поведение вполне ожидаемо, поскольку долговременная память преобразуется функцией активации tanh, а выходной вентиль ограничивает проходящую информацию. Ниже представлен обзор всех 10 нейронов состояния ячейки. Мы видим большое количество светлых клеток, где активация соответствует значениям близким к 0.
В противоположность этому, 10 нейронов скрытого состояния выглядят намного более «сфокусированными». Нейроны #1, #3, #5, #7 вообще имеют нулевые значения активации для первой половины последовательности.
[Рисунок] ссылается на [Страницу]
Вернемся к нейрону #2. Рассмотрим кандидата на добавление в долговременную память и входной вентиль. Величины активации относительно постоянны для каждой половины последовательности – как будто нейрон на каждом шаге вычисляет a += 1 или b += 1.
Ниже представлен обзор всех состояний и вентилей для нейрона #2:
Если вы хотите посмотреть на различные нейроны в данном эксперименте, вы можете воспользоваться веб-приложением.
Обозреватель >>
Следует отметить, что это далеко не единственный вариант результата обучения. LSTM-модель может научиться считать и по-другому. Кроме того, в данном случае я склонен к антропоморфизму. Тем не менее, я думаю, что анализ поведения модели представляет большой интерес и может помочь нам создавать более совершенные архитектуры. Ведь не даром многие идеи в области нейронных сетей основаны на аналогии с мозгом человека, и, если мы видим неожиданное поведение, мы можем попытаться разработать более эффективные механизмы обучения.
Избирательный счет
Теперь рассмотрим более сложный счетчик. Я сгенерировал последовательности следующего вида:
aaXaXaaYbbbbb
Формула последовательности: N символов «a», среди которых случайно распределены символы «X», затем следует разделитель «Y», а далее N символов «b». Модель по-прежнему должна подсчитывать количество символов «a», но на этот раз она должна игнорировать символы «X».
Здесь вы можете увидеть всю LSTM-сеть. По логике мы предполагаем существование считающего нейрона, для которого входной вентиль равен нулю всегда, когда он видит символ «X». И наши ожидания оправдываются!
На рисунке выше мы видим состояние ячейки для нейрона #20. Величина активации увеличивается до разделителя «Y», а затем уменьшается до конца последовательности, как будто он выполняет инкремент переменной на символах «a» и декремент на символах «b».
Если мы посмотрим на входной вентиль, мы увидим, что он действительно игнорирует символы «X»:
Интересная особенность заключается в том, что кандидат на добавление полностью активируется на неинформативных символах «X». Это явление дает нам понять, зачем нужен входной вентиль. Впрочем, если бы в нашей архитектуре не было входного вентиля, вероятно, сеть научилась бы игнорировать «X» каким-либо другим способом, как минимум, в данном простом примере.
Теперь рассмотрим нейрон #10.
Этот нейрон интересен тем, что он активируется только при считывании разделителя «Y». При этом ему все же удается кодировать количество символов «a». (Возможно, на рисунке это не очень хорошо видно, но при считывании символов «Y», принадлежащих последовательностям с одинаковым количеством символов «a», состояние ячейки имеет либо идентичные величины активации, либо находящиеся в пределах 0.1%. На рисунке видно, что символы «Y» светлее в последовательностях с меньшим количеством символов «a».) Вероятно, какой-то другой нейрон видит, что нейрон #10 не справляется, и помогает ему.
Обозреватель >>
Запоминание статуса
Теперь посмотрим, как LSTM-сеть запоминает статус. Для этого я сгенерировал последовательности следующего вида:
AxxxxxxYa
BxxxxxxYb
Формула последовательности: прописной символ «A» или «B», за которым следует 1 – 10 символов «x», затем разделитель «Y», а в конце – строчная версия первого символа.
В этой задаче сеть должна помнить статус: «A» или «B».
Мы предполагаем, что существует нейрон, который активируется, когда помнит, что последовательность началась с символа «A», а также другой аналогичный нейрон для символа «B». Так и есть.
Например, на рисунке ниже представлен «A»-нейрон, который активируется, когда считывает символ «A», и помнит до того момента, когда понадобится сгенерировать последний символ. Обратите внимание, входной вентиль игнорирует все промежуточные символы «x».
На рисунке ниже представлен «B»-нейрон:
Следует отметить один интересный момент: несмотря на то, что знание о статусе «A» или «B» не требуется до того момента, когда сеть прочитает разделитель «Y», скрытое состояние активируется на всех промежуточных входных символах. Это может показаться немного «неэффективным», но, возможно, данное явление объясняется тем, что нейроны выполняют «двойные обязанности», также подсчитывая количество символов «x».
Обозреватель >>
Копирование
Наконец, нам интересно узнать, как LSTM-сеть обучается копировать информацию. Мы видели, как рассмотренная ранее сеть, генерировавшая Java-код, смогла запомнить и скопировать лицензию Apache.
Если задуматься над тем, как работают LSTM-сети, становиться понятно, что запоминание большого количества отдельных детализированных фрагментов информации – не самая сильная их сторона. Например, возможно, вы заметили, что одним серьезным недостатком кода, сгенерированного LSTM-сетью, было использование неопределенных переменных. То есть модель не могла запомнить, какие переменные были в области видимости. Это не удивительно, поскольку с помощью одной ячейки трудно эффективно закодировать многозначную информацию, такую как символы. Кроме того, LSTM-сети не имеют естественного механизма для объединения смежной памяти с целью формирования слов. Сети с памятью (memory network) и нейронные машины Тьюринга (neural Turing machine) являются двумя вариантами развития, позволяющими решить эту проблему с помощью применения компонентов внешней памяти. Однако, несмотря на то, что LSTM-сети не очень хорошо справляются с задачей копирования, все равно интересно понаблюдать за их стараниями.
Для исследования задачи копирования я обучил маленькую 2-слойную LSTM-сеть на последовательностях следующего вида:
baaXbaa
abcXabc
Формула последовательности: 3-сивольный фрагмент, состоящий из символов «a», «b» и «c», затем разделителя «X», затем копия начального фрагмента.
Я не знал точно, как должны выглядеть «копирующие нейроны», поэтому, стремясь найти нейроны, запоминающие части последовательности, я проанализировал скрытые состояния при чтении разделителя «X». Поскольку сеть должна закодировать начальный фрагмент последовательности, ее состояния должны демонстрировать различные шаблоны поведения, в зависимости от того, чему они обучаются.
Например, на рисунке ниже мы видим скрытое состояние нейрона #5 при чтении разделителя «X». Здесь отчетливо видна способность отличать фрагменты последовательности, начинающиеся с символа «c».
В качестве другого примера рассмотрим скрытое состояние нейрона #20 при чтении разделителя «X». Похоже, что он отвечает за последовательность, начинающуюся с символа «b».
Интересный факт: состояние ячейки нейрона #20 самостоятельно кодирует весь 3-символьный фрагмент последовательности. Учитывая одномерность, это просто подвиг!
Ниже представлены состояния ячейки и скрытые состояние нейрона #20 для всех символов последовательности. Отметим, что скрытое состояние бездействует на протяжении всего начального фрагмента последовательности. Вероятно, это явление ожидаемо, поскольку память должна быть просто пассивно сохранена.
Хотя если присмотреться, мы увидим, что на самом деле нейрон активируется всякий раз, когда следующим символом является «b». Таким образом, этот нейрон отвечает не за ситуацию «последовательность началась с “b”», а скорее за ситуацию «следующим символом является “b”».
Насколько можно судить, эта закономерность повторяется во всей сети. Похоже, что все нейроны предсказывают следующий символ, а не запоминают символы на конкретных позициях. Например, нейрон #5 является предиктором для ситуации «следующим символом является “c”».
Я не уверен, является ли такое поведение стандартным для LSTM-сетей при копировании информации. Кроме того, трудно сказать, какие другие механизмы запоминания могут существовать.
Обозреватель >>
Состояния и вентили
Чтобы еще лучше понять назначение состояний и вентилей в LSTM-сети, давайте обсудим некоторые детали рассмотренных экспериментов.
Состояние ячейки и скрытое состояние (память)
Ранее мы определили состояние ячейки, как долговременную память, а скрытое состояние, как рабочую память (элементы долговременной памяти, на которых сеть сфокусирована в данный момент).
Соответственно, если долговременная память в данный момент не востребована, мы ожидаем, что скрытое состояние будет «выключено». И мы действительно наблюдаем это явление в нашем эксперименте по копированию.
Вентиль забывания
Вентиль забывания убирает информацию из состояния ячейки (0 означает полностью забыть, 1 – продолжать полностью помнить). Соответственно, мы ожидаем, что он будет максимально активирован, когда требуется что-либо полностью оставить в памяти, и неактивен, если информация больше никогда не понадобится.
Это явление мы в точности наблюдаем в задаче на запоминание статуса («A»-нейрон). Проходя символы «x», вентиль забывания сильно активируется, чтобы в памяти сохранялся текущий статус «A». Когда же наступает очередь генерировать финальный символ «a», – он переходит в неактивный режим.
Входной вентиль
Как мы говорили ранее, входной вентиль определяет, добавлять или не добавлять новую информацию из входных данных в состояние ячейки. Следовательно, он должен быть неактивен, если информация бесполезна.
Прямое подтверждение мы находим в задаче на избирательный счет. Мы видим, что ведется подсчет количества символов «a» и «b», при этом ненужные символы «X» игнорируются.
Самое удивительное заключается в том, что нигде в уравнениях LSTM-сети мы не указывали, что вентиль забывания, входной вентиль и выходной вентиль должны работать именно таким образом. Модель самостоятельно обучилась этой логике.
Дальнейшее развитие
Теперь давайте повторим все, что мы обсудили.
Во-первых, многие задачи, которые мы хотели бы решить, имеют последовательную или временную природу, поэтому модель должна учитывать прошлые знания. Мы знаем, что скрытые слои нейронной сети эффективно кодируют полезную информацию, так почему бы не использовать эти скрытые слои в качестве памяти, передаваемой от одного момента времени к другому? Таким образом мы получаем рекуррентную нейронную сеть.
Но по собственному поведению мы знаем, что мы не склонны принимать все знания, как данность. Когда мы читаем новую статью о политике, мы не спешим сразу же слепо верить в ее содержание и включать эту информацию в свою картину мира. Мы избирательно решаем, какую информацию запомнить, какую проигнорировать, а какую использовать для принятия решений при следующем чтении новостей. Таким образом, мы хотим, чтобы модель имела возможность обучаться технике сбора, обновления и применения информации. Почему бы не реализовать это с помощью отдельных мини нейронных сетей? Таким образом мы получаем LSTM-сеть.
Теперь, когда мы хорошо разобрались в LSTM-сетях, мы можем подумать об их модификации.
-
Например, возможно, мы считаем, что нерационально иметь две памяти: долговременную и рабочую. Почему бы не объединить их? Мы также можем предположить, что наличие отдельного вентиля забывания и отдельного входного вентиля является избыточным, то есть все, что мы забываем, должно быть заменено на новую информацию, и наоборот. Путем таких рассуждений мы «изобретаем» популярный вариант LSTM-сети под названием GRU.
-
Возможно, мы считаем, что решение о том, какую информацию следует продолжать помнить, какую добавить, а на какой сфокусировать внимание, не должно основываться лишь на рабочей памяти, а должно учитывать также и долговременную память. Подобный ход мыслей приводит нас к концепции Peephole LSTM.
«Вернем нейронным сетям былое величие»
В качестве последнего примера, рассмотрим 2-слойную LSTM-сеть, обученную на твитах Трампа. Модель сумела обнаружить немало интересных закономерностей.
Например, на рисунке ниже представлен нейрон, отслеживающий позицию в пределах хештегов, URL-адресов и @упоминаний:
А вот детектор имен собственных. Обратите внимание, он не просто активируется на словах, написанных с прописной буквы:
Ниже представлен детектор вспомогательных глаголов и глагола «to be» («will be», «I’ve always been», «has never been»):
Детектор авторов цитат:
Существует даже детектор лозунга «Make America Great Again» и прописных букв:
И вот несколько прокламаций, сгенерированных LSTM-сетью (один твит является реальным):
К сожалению, наша модель научилась высказывать не слишком осмысленные вещи.
Заключение
Вот и все. Подведем итог. Вот, что вы изучили:
А вот это вы должны добавить в свою долговременную память:
Выражаю благодарность Чену Ляну (Chen Liang) за использованный мной код, Бену Хемнеру (Ben Hamner) и Kaggle за набор данных с твитами Трампа, и, конечно же, Юргену Шмидхуберу (Jurgen Schmidhuber) и Сеппу Хохрайтеру (Sepp Hochreiter) за их публикацию.
Если вы хотите продолжить исследование рассмотренных нами LSTM-сетей, мое приложение к вашим услугам!
С первой частью статьи вы можете ознакомиться здесь.
Перевод Станислава Петренко