- Оригинальный адрес:РЕКУРРЕНТНАЯ НЕЙРОННАЯ СЕТЬ (RNN) – ЧАСТЬ 4: ИНТЕРФЕЙСЫ ВНИМАНИЯ
- Оригинальный автор:GokuMohandas
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:TobiasLee
- Корректор:changkun Brucexz
Краткое содержание статей из этой серии
- Рекуррентная нейронная сеть RNN, серия 1: базовая RNN против CHAR-RNN
- Рекуррентная нейронная сеть RNN, серия 2: классификация текста
- Рекуррентная нейронная сеть RNN, серия 3: кодировщик, декодер
- Рекуррентная нейронная сеть RNN, серия 4: механизм внимания
- Рекуррентные нейронные сети RNN, серия 5: Пользовательские единицы
Рекуррентная нейронная сеть RNN, серия 4: механизм внимания
В этой статье мы попробуем использовать модель кодер-декодер с механизмом внимания для решения проблемы последовательности к последовательности (seq-seq).Принцип реализации в основном основан на этой статье, конкретный Пожалуйста, обратитесь кздесь.
Сначала рассмотрим архитектуру всей модели и обсудим некоторые интересные части, затем добавим внимание к ранее реализованной модели кодер-декодер без внимания, предыдущая модель Подробности реализации вздесь, мы будем постепенно вводить механизм внимания и реализовывать вывод модели. .Уведомление: Эта модель не самая лучшая, не говоря уже о том, что данные были записаны коряво за несколько минут. Этот пост предназначен для того, чтобы помочь вам понять модели, использующие внимание, чтобы вы могли применять их к большим наборам данных и достигать очень хороших результатов.
Модель кодера-декодера с механизмом внимания:
Это изображение является более конкретной версией первого изображения с более подробной информацией. Начнем с энкодера и закончим выводом декодера. Во-первых, наши входные данные представляют собой вектор, который подвергся заполнению и встраиванию слов, и мы передаем эти векторы в сеть RNN с рядом ячеек (синяя единица RNN на изображении выше), вывод этих ячеек называется скрытым состояние (h на рисунке выше)0,Также1д.), они инициализируются нулем, но после ввода данных эти скрытые состояния изменяются и содержат очень ценную информацию. Если вы используете сеть LSTM (разновидность RNN), мы передаем состояние ячейки c вместе со скрытым состоянием h в следующую ячейку. Для каждого входа (X на рисунке выше0д.), по каждой ячейке мы получаем вывод скрытого состояния, который также является частью ввода для следующей ячейки. Обозначим выход каждого нейрона как h1к чN, и эти выходные данные будут входными данными для нашей модели внимания.
Прежде чем мы углубимся в механизм внимания, давайте посмотрим, как декодер обрабатывает свои входные данные и производит выходные данные. Целевой язык обрабатывается путем встраивания того же слова, что и ввод декодера, начиная с токена GO и заканчивая EOS и некоторым дополнением после него. Ячейка RNN декодера также имеет скрытое состояние и, как и выше, инициализируется нулем и изменяется по мере ввода данных. Таким образом, декодер и кодировщик ничем не отличаются. По сути, они отличаются тем, что декодер также получает контекстный вектор c, сгенерированный механизмом вниманияiв качестве ввода. В следующих разделах мы подробно обсудим, как генерируется вектор контекста, который основан на всех входных данных кодировщика и скрытом состоянии предыдущей ячейки декодера.Очень важный результат: вектор контекста может направлять нас в Как распределить внимание на вводе, сгенерированном кодировщиком, чтобы лучше предсказать следующий вывод.
Каждая ячейка декодера вычисляется с использованием входных данных, сгенерированных кодировщиком, скрытого состояния предыдущей ячейки и вектора контекста, сгенерированного механизмом внимания, и, наконец, окончательный целевой вывод генерируется с помощью функции softmax. Стоит отметить, что во время обучения каждая ячейка RNN использует только эти три выхода для получения вывода цели, однако на этапе вывода мы не знаем, какой будет следующий ввод декодера. Поэтому мы будем использовать предыдущее предсказание декодера в качестве нового входа.
Теперь давайте подробнее рассмотрим, как механизм внимания генерирует вектор контекста.
Механизм внимания:
На приведенном выше рисунке показана схема механизма внимания.Давайте сначала сосредоточимся на входных и выходных частях уровня внимания: мы используем все скрытые состояния, сгенерированные кодировщиком, и выходные данные предыдущей ячейки декодера, чтобы сгенерировать соответствующую ячейку декодера. для каждой ячейки декодера контекстный вектор. Во-первых, эти входные данные проходят через слой функции tanh для создания выходной матрицы e формы [N, H], выход каждой ячейки в кодере создает e, соответствующий i-й ячейке в декодере.ij. Далее, применяя функцию softmax один раз к матрице e, мы получаем вероятность для каждого скрытого состояния, которое мы обозначаем как альфа. Затем используйте альфу, чтобы умножить исходную матрицу скрытого состояния h, чтобы каждое скрытое состояние в каждом h получило вес, и, наконец, просуммируйте, чтобы получить вектор контекста c с формой [N, H]i, который на самом деле является взвешенным представлением ввода, произведенного кодировщиком.
В начале обучения этот вектор контекста может быть произвольным, но по мере обучения наша модель будет продолжать изучать, какая часть входных данных, сгенерированных кодировщиком, важна, помогая нам генерировать лучшие результаты на стороне декодера. .
Реализация тензорного потока:
Теперь реализуем эту модель, важнейшей частью которой является механизм внимания. Мы будем использовать однонаправленный кодер и декодер GRU, как и в предыдущемстатьяОн очень похож на тот, что используется здесь, разница в том, что декодер будет дополнительно использовать вектор контекста (представляющий назначение внимания) в качестве входных данных. Кроме того, мы также будем использовать Tensorflowembedding_attention_decoder()
интерфейс.
Во-первых, давайте взглянем на набор данных, который будет обработан и передан кодировщику/декодеру.
данные:
Я создал небольшой набор данных для модели: 20 английских и соответствующие им испанские предложения. Цель этого руководства — дать вам представление о том, как построить модель кодер-декодер с механизмом мягкого внимания для решения задач последовательного преобразования, таких как машинный перевод. Итак, я написал 20 английских предложений о себе и перевел их испанским аналогам, и это наши данные.
Сначала мы превращаем эти предложения в серию токенов, а затем преобразуем токены в соответствующие идентификаторы словаря. В ходе этого процесса мы создаем словарь словарей, который позволяет нам преобразовывать токены и идентификаторы словарей. Для нашего целевого языка (испанский) мы дополнительно добавим логотип EOS. Затем мы дополним набор токенов, преобразованных из исходного и целевого языков, до их максимальной длины (самая длинная длина предложения в соответствующих наборах данных), что будет последним, что мы передаем данные в нашу модель. Мы передаем дополненные данные исходного языка кодировщику, но мы также выполняем некоторые дополнительные операции над вводом целевого языка, чтобы получить ввод и вывод декодера.
Наконец, ввод выглядит следующим образом:
Это всего лишь пример в наборе данных, 0 в векторе — это дополнение, 1 — это токен GO, а 2 — это токен EOS. Следующий рисунок представляет собой более общее представление процесса обработки данных, вы можете игнорировать часть целевых весов, потому что она не будет использоваться в нашей реализации.
Кодер
мы проходимencoder_inputs
для ввода данных в энкодер. Входные данные представляют собой форму[N, max_len]Матрица , которая становится через вложения слов[N, max_len, H]. Кодировщик представляет собой динамическую RNN, после ее обработки мы получаем форму[N, max_len, H]выход , и матрица состояния формы[N, H](Это состояние, связанное с последней ячейкой после прохождения всех предложений через сеть RNN). Все это будет выводом нашего кодировщика.
декодер
Прежде чем обсуждать механизм внимания, давайте взглянем на ввод и вывод декодера. Начальное состояние декодера передается кодировщиком, а каждое предложение имеет состояние ячейки после прохождения через сеть RNN (в виде[N, H]). Тензорный потокembedding_attention_decoder()
Функция требует, чтобы на входе декодера был упорядоченный список (порядок слов в предложении), поэтому мы поместили[N, max_len]Ввод преобразуется в список длины max_len[N]. Мы также обрабатываем выходные данные декодера, используя матрицу весов softmaxed для создания наших выходных проекционных весов. Мы передаем список временных рядов (т. е. преобразованных [N, max_len]), начальное состояние, матрицу внимания и веса проекций в качестве параметров дляembedding_attention_deocder()
функция, получить вывод (форма[max_len, N, H] Матрица вывода и состояния[N, H]). Выходные данные, которые мы получаем, также расположены в хронологическом порядке, мы сгладим их и применим функцию softmax, чтобы получить форму [Nmax_len, C] матрица. Затем мы также изменяем целевой вывод из[N, max_len]становится **[Nmax_len,]** , повторное использованиеsparse_softmax_cross_entropy_with_logits()
для расчета потерь. Далее мы выполним некоторые операции маскирования потери, чтобы избежать влияния операции заполнения на потерю.
внимание:
Наконец-то мы добрались до части механизма внимания. Теперь, когда мы знаем вход и выход, мы передаем ряд параметров (список временных рядов, начальное состояние, матрица внимания, выход кодировщика) вembedded_attention_decoder()
функцию, но что в ней происходит? Во-первых, мы создадим серию весов для встраивания входных данных, мы назовем эти веса W_embedding. После генерации вывода декодера из ввода мы запускаем функцию цикла, чтобы решить, какую часть вывода передать следующему декодеру в качестве ввода. Во время обучения мы обычно не передаем вывод предыдущего блока декодера следующему, поэтому функция цикла здесь — None. И во время вывода мы делаем это, поэтому функция цикла здесь использует_extract_argmax_and_embed()
, который так же полезен, как следует из его названия (извлекает параметры и встраивает). Получив вывод блока декодера, умножьте его на матрицу весов после softmax (output_projection) и измените его форму с[N, H]преобразовать в[N, C], а затем используйте тот же W_embedding для замены вывода операции внедрения ([N, H]) и использовать обработанный вывод в качестве ввода для следующего блока декодера.
# 如果我们需要预测下一个词语的话,使用如下的循环函数
loop_function = _extract_argmax_and_embed(
W_embedding, output_projection,
update_embedding_for_previous) if feed_previous else None
Еще один необязательный параметр функции цикла:update_embedding_
, если установлено значение False, то мы прекращаем использовать градиентные обновления весов W_embedding, когда выполняем операцию внедрения на выходе декодера (кроме токена GO). Итак, хотя мы используем W_embedding в двух местах, его значение зависит только от вложений слов, которые мы используем на входе декодера, а не на выходе (за исключением токена GO). Затем мы можем передать ввод встроенного временного декодера, начальное состояние, матрицу внимания и функцию цикла вattention_decoder()
функция.
attention_decoder()
Функция является ядром механизма внимания, и в ней есть некоторые дополнительные операции, которые не были упомянуты в начале статьи. Напомним, что механизм внимания будет использовать нашу матрицу внимания (выход кодировщика) и состояние предыдущего блока декодера, и эти значения будут переданы в слой tanh для получения e_ij (используемого для измерения предложения) через скрытая переменная степени выравнивания). Затем мы будем использовать функцию softmax, чтобы преобразовать ее в alpha_ij для умножения суммы на исходную матрицу внимания. Мы суммируем этот умноженный вектор, и это наш новый вектор контекста c_i. В конечном счете, этот вектор контекста будет использоваться для создания выходных данных нашего нового декодера.
Основное отличие состоит в том, что наша матрица внимания (выход кодировщика) и состояние предыдущего блока декодера не просто_linear()
Функция может обрабатывать и применять обычные функции tanh. Нам нужны дополнительные шаги для решения этой проблемы: во-первых, используйте свертку 1x1 в матрице внимания, которая помогает нам извлекать важные функции в матрице внимания вместо прямой обработки исходных данных — вы можете вспомнить важную роль извлечения функций сверточных слоев. в распознавании образов. Этот шаг позволяет нам получить лучшие характеристики, но одна проблема заключается в том, что нам нужно использовать 4-мерный вектор для представления матрицы внимания.
'''
形状转换:
初始的隐藏状态:
[N, max_len, H]
reshape 成 4D 的向量:
[N, max_len, 1, H] = N 张 [max_len, 1, H] 形状的图片
所以我们可以在上面应用滤波器
滤波器:
[1, 1, H, H] = [height, width, depth, # num filters]
使用 stride 为 1 和 padding 为 1 的卷积:
H = ((H - F + 2P) / S) + 1 =
((max_len - 1 + 2)/1) + 1 = height'
W = ((W - F + 2P) / S) + 1 = ((1 - 1 + 2)/1) + 1 = 3
K = K = H
结果就是把
[N, max_len, H] 变成了 [N, height', 3, H]
'''
hidden = tf.reshape(attention_states,
[-1, attn_length, 1, attn_size]) # [N, max_len, 1, H]
hidden_features = []
attention_softmax_weights = []
for a in xrange(num_heads):
# 滤波器
k = tf.get_variable("AttnW_%d" % a,
[1, 1, attn_size, attn_size]) # [1, 1, H, H]
hidden_features.append(tf.nn.conv2d(hidden, k, [1,1,1,1], "SAME"))
attention_softmax_weights.append(tf.get_variable(
"W_attention_softmax_%d" % a, [attn_size]))
Это означает, что для обработки преобразованной 4D-матрицы внимания и предыдущего состояния блока декодера нам также необходимо преобразовать последнее в 4D-представление. Эта операция очень проста, пока состояние предыдущего блока декодера обрабатывается MLP, его можно превратить в 4-мерный тензор, соответствующий преобразованию матрицы внимания.
y = tf.nn.rnn_cell._linear(
args=query, output_size=attn_size, bias=True)
# reshape 成 4 D
y = tf.reshape(y, [-1, 1, 1, attn_size]) # [N, 1, 1, H]
# 计算 Alpha
s = tf.reduce_sum(
attention_softmax_weights[a] *
tf.nn.tanh(hidden_features[a] + y), [2, 3])
a = tf.nn.softmax(s)
# 计算上下文向量 c
c = tf.reduce_sum(tf.reshape(
a, [-1, attn_length, 1, 1])*hidden, [1,2])
cs.append(tf.reshape(c, [-1, attn_size]))
После преобразования как матрицы внимания, так и состояния предыдущего блока декодера мы можем выполнить операцию tanh. Мы умножаем и суммируем результат после tanh и веса, полученного с помощью softmax, и снова применяем функцию softmax, чтобы получить alpha_ij. Наконец, мы изменяем альфа-каналы, умножая исходную матрицу внимания и суммируя ее, чтобы получить вектор контекста c_i.
Затем входы декодера могут обрабатываться один за другим. Давайте сначала обсудим процесс обучения, нас не волнует вывод декодера, потому что ввод в конечном итоге станет выводом, поэтому функция цикла здесь — None. Мы будем использовать_linear()
MLP функции и предыдущий вектор контекста используются для обработки входных данных декодера (инициализированных нулем), а затем передаются в модуль dynamic_rnn вместе с состоянием предыдущего модуля декодера для получения выходных данных. Мы обрабатываем токены одного и того же момента во всех выборочных данных сразу, потому что нам нужно предыдущее состояние, соответствующее последнему токену, проиндексированному с этого момента. Ввод временных рядов позволяет нам делать это более эффективно в пакете данных,ЭтотВот почему нам нужно, чтобы вход стал списком временных рядов.
Получив выходные данные и состояние динамической RNN, мы можем вычислить новый вектор контекста на основе нового состояния. Выходные данные ячейки и новый вектор контекста проходят через MLP, чтобы, наконец, получить выходные данные нашего декодера. Эти дополнительные MLP не показаны на схеме декодера, но они являются дополнительными шагами, необходимыми для получения результата. Стоит отметить, что форма выходных данных ячейки и выходных данных Attention_decoder[max_len, N, H].
И когда мы делаем вывод, функция цикла уже не None, а_extract_argmax_and_append()
. Эта функция будет получать выходные данные предыдущего модуля декодера, а входные данные нашего нового модуля декодера являются результатом предыдущего вывода после софтмаксинга и последующего его повторного встраивания. После всей обработки w с матрицей внимания prev будет обновлен новым предсказанным выходом.
# 依次处理解码器的输入
for i, inp in enumerate(decoder_inputs):
if i > 0:
tf.get_variable_scope().reuse_variables()
if loop_function is not None and prev is not None:
with tf.variable_scope("loop_function", reuse=True):
inp = loop_function(prev, i)
# 把输入和注意力向量合并
input_size = inp.get_shape().with_rank(2)[1]
x = tf.nn.rnn_cell._linear(
args=[inp]+attns, output_size=input_size, bias=True)
# 解码器 RNN
cell_outputs, state = cell(x, state) # our stacked cell
# 通过注意力拿到上下文向量
attns = attention(state)
with tf.variable_scope('attention_output_projection'):
output = tf.nn.rnn_cell._linear(
args=[cell_outputs]+attns, output_size=output_size,
bias=True)
if loop_function is not None:
prev = output
outputs.append(output)
return outputs, state
Затем мы обрабатываем вывод, полученный от Attention_decoder: используем функцию softmax, выполняем операцию сглаживания и, наконец, сравниваем с целевым выводом и вычисляем потери.
деталь:
Sampled Softmax
Использование моделей механизма внимания в задачах последовательного выполнения, таких как машинный перевод, очень эффективно, но часто проблематично из-за огромного корпуса. Особенно, когда мы тренируемся, вычисление softmax вывода декодера очень ресурсоемко, решение состоит в использовании выборочного softmax, которое вы можете найти в моей статье.статьяУзнайте больше о том, почему и как это сделать здесь.
Ниже приведен код для выборки softmax, обратите внимание, что веса здесь такие же, как и output_projection, который мы использовали в декодере, потому что они используются для той же цели: преобразовать вывод декодера (вектор длины H) в соответствующая категория Вектор количественных длин.
def sampled_loss(inputs, labels):
labels = tf.reshape(labels, [-1, 1])
# We need to compute the sampled_softmax_loss using 32bit floats to
# avoid numerical instabilities.
# 我们使用32位的浮点数来计算 sampled_softmax_loss ,以避免数值不稳定
local_w_t = tf.cast(w_t, tf.float32)
local_b = tf.cast(b, tf.float32)
local_inputs = tf.cast(inputs, tf.float32)
return tf.cast(
tf.nn.sampled_softmax_loss(local_w_t, local_b,
local_inputs, labels,
num_samples, self.target_vocab_size),
dtype)
softmax_loss_function = sampled_loss
Далее мы можем использовать функцию seq_loss для расчета потерь, где вектор весов равен 0, за исключением части, где целевым выходом является токен PAD, а остальные равны 1. Стоит отметить, что во время обучения мы используем только выборочный softmax, а во время прогнозирования мы сэмплируем весь корпус, используя обычный softmax, а не только подмножество ближайшего корпуса.
else:
losses.append(sequence_loss(
outputs, targets, weights,
softmax_loss_function=softmax_loss_function))
Модель с ковшами:
Другой распространенной дополнительной конструкцией является использованиеtf.nn.seq2seq.model_with_buckets()
функция, которая также является официальным NMT Tensorflow.руководствоИспользуемая модель, эта модель сегментов, имеет то преимущество, что сокращает длину вектора матрицы внимания. В предыдущей модели мы применяли вектор внимания к скрытым состояниям длины max_len. И здесь нам нужно только применить вектор внимания к соответствующей части, потому что токен PAD может быть полностью проигнорирован. Мы можем выбрать соответствующие корзины так, чтобы в предложении было как можно меньше токенов PAD.
Но я лично считаю этот метод немного грубым, и если вы действительно хотите избежать обработки токенов PAD, я бы рекомендовал использовать атрибут seq_lens для фильтрации токенов PAD в выходных данных кодировщика или при вычислении векторов контекста. установить скрытое состояние, соответствующее токену PAD в каждом предложении, на 0. Этот подход немного сложен, поэтому мы не будем его здесь реализовывать, но сегменты на самом деле не являются элегантным решением проблемы, связанной с токенами PAD.
Суммировать:
Механизмы внимания являются горячей темой исследований, и существует множество вариантов. В любом случае, эта модель всегда очень хорошо справлялась с задачами от последовательности к последовательности, поэтому мне очень нравится ее использовать. Будьте осторожны при разделении наборов для обучения и проверки, так как такие модели могут легко переобучиться и привести к очень низкой производительности на наборе проверки. В следующих статьях мы будем использовать механизм внимания для решения более сложной задачи проектирования памяти и логического мышления.
Код:
Анализ формы матрицы:
Выход энкодера имеет вид[N, max_len], который превращается в[N, max_len, H], а затем передан кодировщику RNN. Выход энкодера имеет вид[N, max_len, H], матрица состояний имеет вид[N, H], который содержит состояние последней ячейки каждой выборки.
Выход кодировщика и форма вектора внимания оба[N, max_len, H].
Выход декодера имеет вид[N, max_len], будет преобразован вmax_lenсписок длин временных рядов, где каждый вектор имеет формуN. Начальное состояние декодера состоит в том, что кодер имеет вид[N, H]матрица состояния. Перед подачей данных в декодер RNN данные встраиваются в список временных рядов длины max_len, где каждый вектор имеет форму [N, H]. Входные данные могут быть фактическими входными данными декодера или, при выполнении прогнозов, выходными данными, произведенными предыдущей ячейкой декодера. Выход, сгенерированный предыдущей ячейкой декодера в предыдущий момент, имеет форму[N, H], пройдет через слой softmax (выходная проекция), чтобы стать[N, C]. Затем используйте вектор веса, который мы использовали на входе, и снова выполните операцию встраивания, чтобы вернуться[N, H]. Эти входные данные будут поданы на декодер RNN, в результате чего декодер формы[max_len, N, H]Матрица вывода и состояния[N, H]. Вывод будет сглажен, чтобы стать[N* max_len, H]И сравните его с целевым выходом, который также был сглажен (также в виде[N* max_len, H]). Если в целевом выводе есть токен PAD, при расчете потерь будут выполняться некоторые операции маскирования, а следующим шагом будет обратное распространение.
Внутри декодера RNN также есть некоторые операции преобразования формы. Сначала вектор внимания (выход кодировщика) имеет форму[N, max_len, H], который будет преобразован в 4-мерный вектор[N, max_len, 1, H](чтобы мы могли использовать операции свертки) и использовать свертки для извлечения полезных функций. Форма этих скрытых функций также четырехмерна.[N, height , 3, H]. Предыдущий вектор скрытого состояния x декодера в форме[N, H], что также является входом в механизм внимания. Этот скрытый вектор состояния преобразуется MLP в[N, H](Причина этого заключается в том, чтобы второе измерение (H) предыдущего скрытого состояния не отличалось от «внимания_размера», который здесь также равен H). Далее вектор скрытого состояния также преобразуется в 4-мерный вектор[N, 1, 1, H], поэтому мы можем комбинировать его со скрытыми функциями. Мы используем функцию tanh для результата сложения, а затем передаем функцию softmax, чтобы получить alpha_ij, форма которого[N, max_len, 1, 1](Это представляет вероятность каждого скрытого состояния в каждой выборке). Эта альфа умножается на исходное скрытое состояние, чтобы получить форму[N, max_len, 1, H]Вектор , а затем суммируется, чтобы получить форму[N, H]вектор контекста .
Вектор контекста и декодер имеют вид[N, H]Комбинация входных данных, независимо от того, являются ли эти входные данные входными данными от декодера (при обучении) или прогнозом из предыдущей ячейки (при прогнозировании), этот ввод представляет собой только длинуmax_lenСписок выглядит так[N, H]один из векторов. Сначала мы добавляем его к предыдущему вектору контекста (инициализируемому всеми 0)[N, H]матрица), напомним, что наши данные на входе декодера представляют собой список временных рядов длиныN, где векторная форма[max_len, ], поэтому форма ввода[N, H]. Результат сложения будет проходить через слой MLP, чтобы получить форму[N, H]Выход этого и матрица состояния (с формой[N, H]) будет передан нашей динамической ячейке RNN. Результирующий вывод cell_outputs имеет вид[N, H], и матрица состояний тоже[N, H]. Эта новая матрица состояний будет входом для нашего следующего декодера. Мы делаем это на входах max_len, в результате чего получается список длины max_len, где все векторы равны [N, H]. После получения этого вывода и матрицы состояния от декодера мы передаем новую матрицу состояния функции внимания, чтобы получить новый вектор контекста, новый вектор контекста имеет форму[N, H], а сумма имеет вид[N, H]Выходы , добавляются, снова применяется MLP, и преобразование принимает вид[N, H]вектор. Наконец, если мы делаем прогнозы, новый prev будет нашим окончательным результатом (prev изначально не имеет значения). prev будет вводом для loop_function, чтобы получить вывод следующего декодера.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,React,внешний интерфейс,задняя часть,продукт,дизайнЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.