Введение
Классификация текстов является очень важным модулем в обработке текстов, и его области применения также очень широки, например: классификация новостей, классификация резюме, классификация почты, классификация офисных документов, региональная классификация и т. д. Быстрое определение и фильтрация информации, отвечающей специальным требованиям. . Он не имеет существенных отличий от других классификаций.Основной метод заключается в том, чтобы сначала извлечь признаки данных классификации, а затем выбрать наилучшее соответствие для классификации.
Вообще говоря, задача классификации текстов относится к классификации текстов в одну или несколько категорий в данной системе классификации. Объекты, подлежащие классификации, — это короткие тексты, такие как предложения, заголовки, обзоры продуктов и т. д., и длинные тексты, такие как статьи. Система классификации обычно делится вручную, например: 1) политика, спорт, военные 2) положительная энергия, отрицательная энергия 3) положительная, нейтральная, отрицательная. Кроме того, существует многоуровневая классификация текста, например, метка блога может быть: обработка естественного языка, классификация текста и т. д. Поэтому соответствующие режимы классификации можно разделить на:двухклассный,Мультикласса такжеМногоуровневая классификацияпроблема.
Задачи классификации текста в основном делятся на:
-
анализ эмоций: предназначен для анализа мнений людей в текстовых данных, таких как обзоры продуктов, обзоры фильмов и твиты, и извлечения их полярностей и мнений. Это может быть бинарная или многоклассовая проблема, бинарный анализ настроений должен классифицировать текст на положительные и отрицательные классы, в то время как многоклассовый анализ настроений фокусируется на классификации данных по мелким меткам или многоуровневым уровням интенсивности.
-
категории новостей: Система классификации новостей может помочь пользователям получать интересную информацию в режиме реального времени. Идентификация темы новостей на основе интересов пользователя и рекомендации по связанным новостям - два основных применения классификации новостей.
-
Тематический анализ: Тематический анализ пытается автоматически извлечь смысл из текстов, определяя их темы. Цель классификации тем — назначить одну или несколько тем каждому документу для облегчения анализа.
-
Система ответов на вопросы: Существует два типа систем ответов на вопросы: экстрактивные и генеративные. Извлекающий QA можно рассматривать как частный случай классификации текста. Имея вопрос и набор вариантов ответов, правильно классифицируйте каждый вариант ответа в соответствии с требованиями вопроса.
-
вывод на естественном языке: NLI, также известный как распознавание текстового следования (RTE), предсказывает, можно ли вывести значение одного текста из другого текста. В частности, системе необходимо присвоить метку каждой паре текстовых единиц, например следствию, противоречию и нейтральности.
Как правило, существует три основных подхода к классификации текстов:
-
Метод, основанный на сопоставлении характеристик правил (например, оценка эмоций на основе специальных слов, таких как «нравится», «ненависть» и т. д., но с низкой точностью, обычно как метод, помогающий судить)
-
Методы, основанные на традиционном машинном обучении (конструирование признаков + алгоритмы классификации)
- Метод на основе глубокого обучения (вектор слов + нейронная сеть)
Классификация текстов на основе методов глубокого обучения
FastText
бумага:АР Вест V.org/ABS/1607.01…
Код:GitHub.com/Facebook Рес…
FastText
Это инструмент для расчета векторов слов и классификации текста, открытый Facebook в 2016 году, который характеризуется высокой скоростью обучения. В задаче классификации текстаFastText
(Мелкие сети) часто достигают точности, сравнимой с глубокими сетями, но на порядки быстрее, чем глубокие сети, по времени обучения. На стандартном многоядерном процессоре векторы слов в корпусе из 1 миллиарда слов могут быть обучены в течение 10 минут, а более 500 000 предложений с более чем 300 000 категорий могут быть классифицированы в течение 1 минуты.
FastText
Это быстрый алгоритм классификации текста, который имеет два основных преимущества перед алгоритмами классификации на основе нейронных сетей:
-
FastText
Ускоряет обучение и тестирование, сохраняя при этом высокую точность -
FastText
Нет необходимости в предварительно обученных векторах слов,FastText
Обучит вектор слова сам по себе -
FastText
Две важные оптимизации:Hierarchical Softmax
,N-gram
FastText
типовая архитектура иword2vec
серединаCBOW
очень похоже, разница естьFastText
предсказывать метки, покаCBOW
Прогнозы — это промежуточные слова, то есть архитектура моделей похожа, но задачи у моделей разные:
word2vec
Преобразование контекстных отношений в задачи мультиклассификации.В общих текстовых данных тезаурус колеблется от десятков тысяч до миллионов.Нереально напрямую обучать мультиклассовую логистическую регрессию во время обучения.word2vec
Два метода оптимизации для крупномасштабных задач множественной классификации представлены вNegative sampling
иHierarchical softmax
. В оптимизации,Negative sampling
Обновляются только векторы слов небольшого числа отрицательных слов, что снижает объем вычислений.Hierarchical softmax
Тезаурус представляется в виде префиксного дерева, а путь от корня к листу может быть представлен в виде последовательности бинарных классификаторов.Сложность расчета одной мультиклассификации составляет отопущен на высоту дерева.
FastText
Архитектура модели: гдезначит в текстеn-gram
вектор, каждая функция является средним значением векторов слов.
слабость это:
Я не люблю такие фильмы, но этот понравился.
Я люблю такие фильмы, но не этот.
Такие два предложения абсолютно одинаковы, когда они отправляются в однослойную нейронную сеть после усреднения вектора слов. Классификатор не может различить разницу между двумя предложениями. Только путем добавленияn-gram
Характеристики могут отличаться позже. Поэтому в практическом применении необходимо иметь достаточное представление о данных, а затем выбирать модель.
Для сцен с длинным текстом и высокими требованиями к скоростиFasttext
даBaseline
Предпочтительно. В то же время его также хорошо использовать для обучения векторов слов на неконтролируемых корпусах для текстового представления. Однако для дальнейшего улучшения эффекта требуются более сложные модели.
Структура модели
-
Ввод модели:
[batch_size, seq_len]
-
embedding
Слой: случайная инициализация, размер вектора словаembed_size
:[batch_size, seq_len, embed_size]
-
просить обо всем
seq_len
Среднее количество слов:[batch_size, embed_size]
-
Полностью подключен +
softmax
Нормализованный:[batch_size,num_class]
основной код
class FastText(nn.Module):
def __init__(self, config, word_embedding, freeze):
"""
config.class_num: 类别数
word_embedding: 训练好的词向量(一个类,包含idx2str、str2idx、vectors)
freeze: 是否冻结词向量
"""
super(FastText, self).__init__()
word_embedding = word_embedding
self.embedding_size = len(word_embedding.vectors[0])
# 加载词向量
self.embedding = nn.Embedding.from_pretrained(torch.from_numpy(np.asarray(word_embedding.vectors)),
freeze=freeze)
self.fc = nn.Linear(self.embedding_size, config.class_num)
def forward(self, input_ids):
# text = [batch size, sent len]
embedded = self.embedding(input_ids).float()
# embedded = [batch size, sent len, emb dim]
pooled = F.avg_pool2d(embedded, (embedded.shape[1], 1)).squeeze(1)
# pooled = [batch size, embedding_dim]
return self.fc(pooled), pooled
TextCNN
бумага:АР Вест V.org/ABS/1408.58…
Код:GitHub.com/Ю и Ким/CNN…
TextCNN
Это модель, предложенная Юн Кимом в 2014 году, которая впервые применилаCNN
кодированиеn-gram
Первая в своем роде функция.
Подробный процесс:
-
Embedding
: первый слой самый левый на картинкеМатрица предложения , каждая строка представляет собой вектор слов, а размерность, которые можно сравнить с исходными пикселями изображения. -
Convolution
: затем послеkernel_sizes=(2,3,4)
1D сверточные слои, каждыйkernel_size
имеютвыходыchannel
. -
MaxPolling
: Третий этаж1-max pooling
слой (потому что это измерение времени, также называемоеmax-over-time pooling
), так что предложения разной длины проходят черезpooling
Затем слой можно превратить в представление фиксированной длины. -
Full Connection and Softmax
: Последний слой полностью связанsoftmax
слой, который выводит вероятность каждого класса.
существуетTextCNN
На практике есть много мест для оптимизации:
-
kernel_sizes
размер: Этот параметр определяет извлечениеn-gram
Длина признака, этот параметр в основном связан с данными, средняя длинаЕсли внутри, используйтеСледующее в порядке, в противном случае это может быть дольше. Вы можете сначала использовать размер при настройке параметровgrid search
, найдите оптимальный размер, затем попробуйте сочетание оптимального размера и ближайших размеров -
Filter
номер: этот параметр влияет на размер конечного элемента. Если размер слишком велик, скорость обучения будет ниже. здесь, вВы можете настроить параметры между -
CNN
активационная функция:Можешь попытатьсяReLU
,tanh
-
Регуляризация: укажи вправо
CNN
Регуляризация параметров, вы можете использоватьdropout
илиL2
, но эффект очень маленький, можно попробовать небольшойdropout
-
Pooling
Способ: выбираем по ситуацииmean
,max
,k-max pooling
,большую часть времениmax
Производительность очень хорошая, потому что задача классификации не требует высокой детализации семантики, и фиксируется только самый крупный признак. -
Embedding
Таблица: можно выбрать китайскийchar
илиword
Ввод уровня также можно использовать для обоих, что улучшит эффект. Если обучающих данных достаточно (), так же можно тренироваться с нуля -
Дистилляция
BERT
изlogits
, используя неконтролируемые данные в домене
TextCNN
очень подходитКороткая и средняя текстовая сценасильныйbaseline
,ноНе подходит для длинных текстов, так как размер ядра свертки обычно не очень большой,Невозможно захватить объекты на большом расстоянии. В то же время max-pooling также имеет ограничения, теряя структурную информацию текста, поэтому сложно найти в тексте сложные закономерности, такие как поворотные отношения. . Кроме того, если хорошенько подумать,TextCNN
и традиционныеn-gram
Суть модели мешка слов та же, и большая часть ее положительного эффекта связана с введением векторов слов, которые решают проблему разреженности модели мешка слов.
Структура модели
основной код
class TextCNN(nn.Module):
def __init__(self, config, word_embedding, freeze):
"""
config.n_filters: 卷积核个数(对应2维卷积的通道数)
config.filter_sizes: 卷积核的多个尺寸
config.class_num: 类别数
config.dropout: dropout率
word_embedding: 训练好的词向量(一个类,包含idx2str、str2idx、vectors)
freeze: 是否冻结词向量
"""
super(TextCNN, self).__init__()
word_embedding = word_embedding
self.embedding_size = len(word_embedding.vectors[0])
self.embedding = nn.Embedding.from_pretrained(torch.from_numpy(np.asarray(word_embedding.vectors)),
freeze=freeze)
self.convs = nn.ModuleList(
[nn.Conv2d(in_channels=1, out_channels=config.n_filters,
kernel_size=(fs, self.embedding_size)) for fs in config.filter_sizes])
self.fc = nn.Linear(len(config.filter_sizes) * config.n_filters, config.class_num)
self.dropout = nn.Dropout(config.dropout)
def forward(self, input_ids):
# text = [batch size, sent len]
embedded = self.embedding(input_ids)
# embedded = [batch size, sent len, emb dim]
embedded = embedded.unsqueeze(1).float()
# embedded = [batch size, 1, sent len, emb dim]
conved = [F.relu(conv(embedded)).squeeze(3) for conv in self.convs]
# conved_n = [batch size, n_filters, sent len - filter_sizes[n] + 1]
pooled = [F.max_pool1d(conv, int(conv.shape[2])).squeeze(2) for conv in conved]
# pooled_n = [batch size, n_filters]
cat = self.dropout(torch.cat(pooled, dim=1))
# cat = [batch size, n_filters * len(filter_sizes)]
return self.fc(cat)
DPCNN
бумага:Love.Tencent.com/Arab/Media…
Код:GitHub.com/649453932/C…
ACL
В середине 2017 года TencentAI-lab
придумалDeep Pyramid Convolutional Neural Networks for Text Categorization(DPCNN)
. В статье предлагается метод, основанный наword-level
уровень сети из-заTextCNN
Междугородные зависимости текста не могут быть получены с помощью свертки, а бумагаDPCNN
Постоянно углубляя сеть, можно извлечь долгосрочные текстовые зависимости. Эксперименты показывают, что наилучшую точность можно получить, увеличив глубину сети без слишком больших вычислительных затрат.
Region embedding
Автор будетTextCNN
Результат свертки сверточного слоя, содержащего многоразмерные сверточные фильтры, называетсяRegion embedding
, что означает, что для текстовой области/фрагмента (например,3-gram
), созданный после набора операций сверткиembedding
.
Компромисс между сверточной и полносвязной
производитьregion embedding
После этого, по классическомуTextCNN
Метод состоит в том, чтобы выбрать наиболее репрезентативные функции из каждой карты функций, то есть напрямую применить глобальный слой максимального объединения, чтобы сгенерировать вектор функций этого текста, если сверточный фильтрsize
имеют(3,4,5)
эти трое, каждыйsize
ВключаютЯдро свертки, то конечно выдасткарты объектов, а затемmax-over-time-pooling
Операция применяется к каждой карте объектов, поэтому вектор признаков текста равенизмерение.
TextCNN
Смысл этого в основном такой же, как у классической модели классификации текста модели мешка слов, ноone-hot
прибытьword embedding
Преобразование представления позволяет избежать проблемы разреженности данных, с которой сталкиваются модели мешка слов.TextCNN
В сущности, синонимы, возникающие при введении словесных векторов, представляются подобными векторамиbonus
,в то же времяTextCNN
Это может лучше использовать синонимические отношения в векторе слова.
Информация, передаваемая на большие расстояния, которую трудно усвоить в классической модели,TextCNN
все еще трудно учиться.
изометрическая свертка
Предположим, что длина входной последовательности равна, размер ядра свертки, размер шага, заполнение на обоих концах входной последовательностинулей, то выходная последовательность сверточного слоя имеет вид.
- Узкая свертка: шаг, оба конца не заполнены нулями, т.е., выходная длина после свертки равна.
- Широкая свертка: шаг, с нулями на обоих концах, выходная длина после свертки.
- Свертка равной длины: шаг, с нулями на обоих концах, выходная длина после свертки равна.
Первая последовательность ввода-выводаКусокembedding
называется первымлексическая позиция, то тогдаsize=n
Смысл свертки равной длины, порожденной ядром свертки, состоит в объединении каждой лексемы входной последовательности и ее левого и правогоКонтекстная информация каждого слова сжата в лексему.embedding
, что приводит к более высокому уровню и более точной семантике каждой лексемы, модифицированной контекстной информацией. хочу преодолетьTextCNN
Недостаток съемки в дальнем режиме, очевидно, заключается в использовании глубокойCNN
.
Прямой стек свертки равной длиныСвертка равной длины заставит каждую лексему содержать все больше и больше и более длинную контекстную информацию.Этот метод сделает количество слоев сети очень-очень-очень глубоким, но этот метод слишком громоздкий. Однако, поскольку стек свертки равной длины, свертка равной длины сделает каждую лексемуembedding
Семантическое описание богаче и точнее, а два слоя можно соответствующим образом сложить для улучшения лексемы.embedding
богатство представлений.
фиксированныйfeature map
количество
После выражения семантики каждой лексемы многие соседние слова или смежныеngram
Значение слова можно комбинировать, например, «не надо» и «слишком хорошо» в «Сяомин люди не слишком хороши». Хотя семантика изначально очень далека, когда они появляются как соседнее слово «не слишком хорошо», их семантика в основном эквивалентна «Очень хорошо», можно комбинировать семантику «нельзя» и «слишком хорошо». В то же время процесс слияния может быть полностьюembedding space
В исходном тексте очень даже можно напрямую слить «не слишком хорошо» в «очень хорошо», и совершенно незачем сдвигать все смысловое пространство.
На самом деле, по сравнению с очевидным иерархическим различием признаков в изображениях от признаков низкого уровня, таких как «точки, линии, дуги», до признаков высокого уровня, таких как «глаза, нос, рот», признаки в тексте более продвинуты. порядок явно гораздо более плоский, т.е. от слова (1gram
) к фразе к3gram
,4gram
На самом деле он в значительной степени удовлетворяет характеристикам «семантической замены». Такого рода явление «семантической замены» трудно встретить в изображениях. следовательно,DPCNN
иResNet
Большая разница в том, чтосуществуетDPCNN
фиксированный мертвыйfeature map
количество, то есть фиксированныйembedding space
Размерность (для простоты понимания в дальнейшем именуемая семантическим пространством), позволяющая сети сделать все соседние слова (соседние слова)ngram
) выполняется в исходном пространстве или пространстве, аналогичном исходному пространству (конечно, будет ли это делать сеть на практике, не обязательно, а лишь обеспечивает такое условие). То есть, хотя вся сеть имеет глубокую форму, она может быть совершенно плоской с точки зрения семантического пространства. иResNet
Это постоянное изменение семантического пространства, чтобы семантика изображения продолжала прыгать в семантическое пространство более высокого уровня по мере углубления сетевого слоя.
объединение
Каждый раз, когда вы проходитеСлой пула (называемыйобъединяющий слой), длина последовательности сжимается до половины исходной. Это тоже самоеЯдро свертки , каждый раз послеПосле слоя объединения воспринимаемые текстовые фрагменты в два раза длиннее, чем раньше. Например, раньше это было заметно толькоинформация о длине лексемы, послеПосле того, как объединяющий слой сможет восприниматьинформацию о длине лексемы, затем поместитеобъединяющий слой иsize=3
Сверточные слои объединяются, как показано на рисунке:
остаточное соединение
на глубине инициализацииCNN
При часто веса каждого слоя инициализируются малым значением, что приводит к тому, что в исходной сети вход почти каждого последующего слоя близок кВ это время выход сети, естественно, не имеет смысла, и эти малые веса также препятствуют распространению градиента, так что начальная фаза обучения сети часто начинается долго. В то же время, даже если сеть запущена, из-за приближенного непрерывного умножения аффинной матрицы в глубокой сети сеть очень подвержена проблемам градиентного взрыва или дисперсии в процессе обучения (хотя из-за неразделяемых весов , глубинаCNN
коэффициент сетиRNN
лучшая сеть).
для глубиныCNN
Проблема градиентной диффузии для сетейResNet
предложено вshortcut-connection\skip-connection\residual-connection
(остаточные соединения) — очень простое, разумное и эффективное решение.
основной код
class DPCNN(nn.Module):
def __init__(self, config, word_embedding, freeze):
"""
config.n_filters: 卷积核个数(对应2维卷积的通道数)
config.class_num: 类别数
word_embedding: 训练好的词向量(一个类,包含idx2str、str2idx、vectors)
freeze: 是否冻结词向量
"""
super(DPCNN, self).__init__()
word_embedding = word_embedding
self.embedding_size = len(word_embedding.vectors[0])
self.embedding = nn.Embedding.from_pretrained(torch.from_numpy(np.asarray(word_embedding.vectors)),
freeze=freeze)
self.conv_region = nn.Conv2d(1, config.n_filters, (3, self.embedding_size), stride=1)
self.conv = nn.Conv2d(config.n_filters, config.n_filters, (3, 1), stride=1)
self.max_pool = nn.MaxPool2d(kernel_size=(3, 1), stride=2)
self.padding1 = nn.ZeroPad2d((0, 0, 1, 1)) # top bottom
self.padding2 = nn.ZeroPad2d((0, 0, 0, 1)) # bottom
self.relu = nn.ReLU()
self.fc = nn.Linear(config.n_filters, config.class_num)
def forward(self, input_ids):
# [batch size, seq len, emb dim]
x = self.embedding(input_ids)
# [batch size, 1, seq len, emb dim]
x = x.unsqueeze(1).to(torch.float32)
# [batch size, n_filters, seq len-3+1, 1]
x = self.conv_region(x)
# [batch size, n_filters, seq len, 1]
x = self.padding1(x)
x = self.relu(x)
# [batch size, n_filters, seq len-3+1, 1]
x = self.conv(x)
# [batch size, n_filters, seq len, 1]
x = self.padding1(x)
x = self.relu(x)
# [batch size, n_filters, seq len-3+1, 1]
x = self.conv(x)
# [batch size, n_filters, 1, 1]
while x.size()[2] >= 2:
x = self._block(x)
# [batch size, n_filters]
x_embedding = x.squeeze()
# [batch_size, 1]
x = self.fc(x_embedding)
return x
def _block(self, x):
x = self.padding2(x)
px = self.max_pool(x)
x = self.padding1(px)
x = F.relu(x)
x = self.conv(x)
x = self.padding1(x)
x = F.relu(x)
x = self.conv(x)
# Short Cut
x = x + px
return x
TextRNN
RNN
Рекуррентная нейронная сеть (Recurrent Neural Network,RNN
) это класс с кратковременной памятью
силовая нейронная сеть. В рекуррентной нейронной сети нейроны могут не только получать информацию от других нейронов, но и
Он может принимать свою собственную информацию и формировать сетевую структуру с петлями.
долгосрочные зависимости
Хотя простая рекуррентная сеть теоретически может устанавливать зависимости между состояниями через большие промежутки времени,
Именно из-за проблемы взрывающегося или исчезающего градиента фактически можно изучить только краткосрочные зависимости. Таким образом, если моментВыводзависит от моментаввод, когда интервалКогда она относительно велика, простой нейронной сети сложно моделировать такие дальнодействующие зависимости, что называется проблемой дальнодействующих зависимостей (Long-Term Dependencies Problem
).
LSTM
Чтобы улучшить проблему дальней зависимости рекуррентных нейронных сетей, очень хорошим решением является введение механизма стробирования для управления скоростью накопления информации, включая выборочное добавление новой информации и выборочное забвение ранее накопленной информации.
долговременная память (Long Short-Term Memory,LSTM
) сети [Gers et al; Hochreiter
et al., 2000; 1997] — это вариант рекуррентных нейронных сетей, который может эффективно решать проблему взрывающегося или исчезающего градиента простых рекуррентных нейронных сетей.
LSTM
Ключ лежит в состоянии клеткиИ линия, которая проходит через ячейку, состояние ячейки похоже на конвейерную ленту, бегущую непосредственно по всей цепочке, всего с несколькими небольшими линейными взаимодействиями, становится легко протекающей по ней информации оставаться неизменной.
LSTM
В сети представлен механизм ворот (Gating Mechanism
) для контроля пути передачи информации.LSTM
«Гейт» в сети — это своего рода «мягкий» гейт, значение которого вМежду ними это означает, что информация передается в определенном соотношении.
Забытые ворота
Забытые воротаКонтролируйте внутреннее состояние предыдущего моментаСколько информации нужно забыть.
входные ворота
Входной вентиль определяет, сколько новой информации добавляется к текущей.Войдите. Для этого необходимо выполнить два шага:
- Сначала вычислите входной вентильи возможные состояния ячейки памяти.
- Объедините ворота забвенияи входные воротаобновить блок памяти
выходные ворота
В конечном итоге нам нужно определить, какое значение выводить, которое будет основано на состоянии наших ячеек, а также на отфильтрованной версии. Сначала мы проходимsigmoid
слой, чтобы определить, какие части состояния ячейки будут выводиться. Далее мы передаем состояние ячейки черезtanh
процесс (получитьзначение) и это иsigmoid
Выходы вентилей перемножаются.
GRU
существуетLSTM
Введены три функции ворот: входные ворота, забывающие ворота и выходные ворота для управления передачей информации.Поскольку входные ворота и ворота забывания дополняют друг друга, они имеют определенную степень избыточности.GRU
Интернет
напрямую использовать гейт для управления балансом между вводом и забыванием, вGRU
В модели есть только два шлюза: шлюз обновления и шлюз сброса.
на картинкеипредставляют шлюз обновления и шлюз сброса соответственно. Шлюз обновления используется для управления степенью, в которой информация о состоянии предыдущего момента приводится в текущее состояние.Чем больше значение шлюза обновления, тем больше информации о состоянии предыдущего момента вводится. Ворота сброса контролируют, сколько информации из предыдущего состояния записывается в текущее состояние-кандидат.С другой стороны, чем меньше вентиль сброса, тем меньше информации из предыдущего состояния записывается.
LSTM
иGRU
Все важные функции сохраняются благодаря различным функциям ворот, что гарантирует, что вlong-term
Он не теряется при распространении. такжеGRU
относительноLSTM
Одной вентильной функцией меньше, поэтому количество параметров также меньше, чемLSTM
, так в целомGRU
обучение проходит быстрее, чемLSTM
из.
Attention
Attention
рассчитать:
вскрытое состояние, полученное для каждого момента,заcontext vector
, случайным образом инициализируемым и обновляемым с помощью обучения, и, наконец, получаем представление предложения, а затем классифицировать.
основной код
class RNNAttention(nn.Module):
def __init__(self, config, word_embedding, freeze, batch_first=True):
"""
config.class_num: 类别数
config.hidden_dim: rnn隐藏层的维度
config.n_layers: rnn层数
config.rnn_type: rnn类型,包括['lstm', 'gru', 'rnn']
config.bidirectional: 是否双向
config.dropout: dropout率
word_embedding: 训练好的词向量(一个类,包含idx2str、str2idx、vectors)
freeze: 是否冻结词向量
batch_first: 第一个维度是否是批量大小
"""
super(RNNAttention, self).__init__()
word_embedding = word_embedding
self.embedding_size = len(word_embedding.vectors[0])
self.embedding = nn.Embedding.from_pretrained(torch.from_numpy(np.asarray(word_embedding.vectors)),
freeze=freeze)
if config.rnn_type == 'lstm':
self.rnn = nn.LSTM(self.embedding_size,
config.hidden_dim,
num_layers=config.n_layers,
bidirectional=config.bidirectional,
batch_first=batch_first,
dropout=config.dropout)
elif config.rnn_type == 'gru':
self.rnn = nn.GRU(self.embedding_size,
hidden_size=config.hidden_dim,
num_layers=config.n_layers,
bidirectional=config.bidirectional,
batch_first=batch_first,
dropout=config.dropout)
else:
self.rnn = nn.RNN(self.embedding_size,
hidden_size=config.hidden_dim,
num_layers=config.n_layers,
bidirectional=config.bidirectional,
batch_first=batch_first,
dropout=config.dropout)
# query向量
self.u = nn.Parameter(torch.randn(config.hidden_dim * 2), requires_grad=True)
self.tanh = nn.Tanh()
self.fc = nn.Linear(config.hidden_dim * 2, config.class_num)
self.dropout = nn.Dropout(config.dropout)
self.batch_first = batch_first
def forward(self, text, text_lengths):
# 按照句子长度从大到小排序
text, sorted_seq_lengths, desorted_indices = self.prepare_pack_padded_sequence(text, text_lengths)
# text = [batch size, sent len]
embedded = self.dropout(self.embedding(text)).float()
# embedded = [batch size, sent len, emb dim]
# pack sequence
packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, sorted_seq_lengths, batch_first=self.batch_first)
self.rnn.flatten_parameters()
if config.rnn_type in ['rnn', 'gru']:
packed_output, hidden = self.rnn(packed_embedded)
else:
# output (batch, seq_len, num_directions * hidden_dim)
# hidden (batch, num_layers * num_directions, hidden_dim)
packed_output, (hidden, cell) = self.rnn(packed_embedded)
# unpack sequence
output, output_lengths = nn.utils.rnn.pad_packed_sequence(packed_output, batch_first=self.batch_first)
# 把句子序列再调整成输入时的顺序
output = output[desorted_indices]
# output = [batch_size, seq_len, hidden_dim * num_directionns ]
alpha = F.softmax(torch.matmul(self.tanh(output), self.u), dim=1).unsqueeze(-1)
# alpha = [batch_size, seq_len, 1]
output_attention = output * alpha # [batch_size, seq_len, hidden_dim * num_directionns ]
output_attention = torch.sum(output_attention, dim=1) # [batch_size, hidden_dim]
fc_input = self.dropout(output_attention)
out = self.fc(fc_input)
return out, fc_input
def prepare_pack_padded_sequence(self, inputs_words, seq_lengths, descending=True):
"""
for rnn model :按照句子长度从大到小排序
"""
sorted_seq_lengths, indices = torch.sort(seq_lengths, descending=descending)
_, desorted_indices = torch.sort(indices, descending=False)
sorted_inputs_words = inputs_words[indices]
return sorted_inputs_words, sorted_seq_lengths, desorted_indices
TextRCNN
бумага:Войдите на .ACM.org/do i/10.5555…
Код:GitHub.com/649453932/C…
RNN
иCNN
Как основные архитектуры моделей для задач классификации текста, все они имеют свои преимущества и ограничения.RNN
Хорошо справляется со структурой последовательности, может учитывать контекстную информацию предложений, ноRNN
принадлежатьbiased model
, последние слова в предложении более важны, что может повлиять на окончательный результат классификации, потому что слова, оказывающие наибольшее влияние на классификацию предложения, могут располагаться в любом месте предложения.CNN
Он принадлежит к беспристрастной модели и может получить наиболее важные функции за счет максимального объединения, ноCNN
Размер скользящего окна определить непросто.Если выбор слишком мал, легко потерять важную информацию.Если выбор слишком велик, это приведет к огромному пространству параметров. Чтобы устранить ограничения обоих, в этой статье предлагается новая сетевая архитектура, которая использует двунаправленную рекуррентную структуру для получения контекстной информации, которая может уменьшить шум больше, чем традиционные нейронные сети на основе окна, и может быть больше при изучении текстовых представлений.Зарезервированный порядок слов для диапазонов. Во-вторых, используйте слой максимального объединения, чтобы получить важные части текста и автоматически определить, какой признак играет более важную роль в процессе классификации текста.
обучение представлению слов
Автор предлагает сочетать левый контекст слова, правый контекст и само слово как словесную репрезентацию. Автор использует двустороннийRNN
для извлечения контекстной информации предложений соответственно. Формула выглядит следующим образом:
в,репрезентативное словолевый контекст ,левый контекст по предыдущему словуи вектор вложения слова предыдущего словаВычисленный, как показано в формуле (1), левый контекст первого слова всех предложений использует одни и те же общие параметры.Используется для объединения семантики левого контекста предыдущего слова и семантики предыдущего слова со словом.в левом контекстном представлении . Обработка правого контекста точно такая же, как и левого контекста, и те же общие параметры используются для правого контекста последнего слова всех предложений.. После получения левого и правого контекстных представлений каждого слова в предложении слово может быть определено.выражается следующим образом
на самом деле слова, вектор представления слова, встраивающий словои правильный вектор контекста словасклеенный результат. получитьпредставительствоПосле этого вы можете войти в функцию активации, чтобы получитьскрытый семантический вектор.
Изучение текстового представления
После прохождения сверточных слоев получаются представления всех слов, и они сначала подвергаются операции max-pooling, которая может помочь найти в предложении наиболее важную скрытую семантическую информацию.
Затем пройдите через полносвязный слой, чтобы получить представление текста, и, наконец, передайтеsoftmax
слои классифицируются.
основной код
class RCNN(nn.Module):
def __init__(self, config, word_embedding, freeze, batch_first=True):
"""
config.class_num: 类别数
config.hidden_dim: rnn隐藏层的维度
config.n_layers: rnn层数
config.rnn_type: rnn类型,包括['lstm', 'gru', 'rnn']
config.bidirectional: 是否双向
config.dropout: dropout率
word_embedding: 训练好的词向量(一个类,包含idx2str、str2idx、vectors)
freeze: 是否冻结词向量
batch_first: 第一个维度是否是批量大小
"""
super(RCNN, self).__init__()
word_embedding = word_embedding
self.embedding_size = len(word_embedding.vectors[0])
self.embedding = nn.Embedding.from_pretrained(torch.from_numpy(np.asarray(word_embedding.vectors)),
freeze=freeze)
if config.rnn_type == 'lstm':
self.rnn = nn.LSTM(self.embedding_size,
config.hidden_dim,
num_layers=config.n_layers,
bidirectional=config.bidirectional,
batch_first=batch_first,
dropout=config.dropout)
elif config.rnn_type == 'gru':
self.rnn = nn.GRU(self.embedding_size,
hidden_size=config.hidden_dim,
num_layers=config.n_layers,
bidirectional=config.bidirectional,
batch_first=batch_first,
dropout=config.dropout)
else:
self.rnn = nn.RNN(self.embedding_size,
hidden_size=config.hidden_dim,
num_layers=config.n_layers,
bidirectional=config.bidirectional,
batch_first=batch_first,
dropout=config.dropout)
# 1 x 1 卷积等价于全连接层,故此处使用全连接层代替
self.fc_cat = nn.Linear(config.hidden_dim * 2 + self.embedding_size, self.embedding_size)
self.fc = nn.Linear(self.embedding_size, config.class_num)
self.dropout = nn.Dropout(config.dropout)
self.batch_first = batch_first
def forward(self, text, text_lengths):
# 按照句子长度从大到小排序
text, sorted_seq_lengths, desorted_indices = self.prepare_pack_padded_sequence(text, text_lengths)
# text = [batch size, sent len]
embedded = self.dropout(self.embedding(text)).float()
# embedded = [batch size, sent len, emb dim]
# pack sequence
packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, sorted_seq_lengths, batch_first=self.batch_first)
self.rnn.flatten_parameters()
if config.rnn_type in ['rnn', 'gru']:
packed_output, hidden = self.rnn(packed_embedded)
else:
# output (batch, seq_len, num_directions * hidden_dim)
# hidden (batch, num_layers * num_directions, hidden_dim)
packed_output, (hidden, cell) = self.rnn(packed_embedded)
# unpack sequence
output, output_lengths = nn.utils.rnn.pad_packed_sequence(packed_output, batch_first=self.batch_first)
# 把句子序列再调整成输入时的顺序
output = output[desorted_indices]
# output = [batch_size, seq_len, hidden_dim * num_directionns ]
batch_size, max_seq_len, hidden_dim = output.shape
# 拼接左右上下文信息
output = torch.tanh(self.fc_cat(torch.cat((output, embedded), dim=2)))
# output = [batch_size, seq_len, embedding_size]
output = torch.transpose(output, 1, 2)
output = F.max_pool1d(output, int(max_seq_len)).squeeze().contiguous()
return self.fc(output)
def prepare_pack_padded_sequence(self, inputs_words, seq_lengths, descending=True):
"""
for rnn model :按照句子长度从大到小排序
"""
sorted_seq_lengths, indices = torch.sort(seq_lengths, descending=descending)
_, desorted_indices = torch.sort(indices, descending=False)
sorted_inputs_words = inputs_words[indices]
return sorted_inputs_words, sorted_seq_lengths, desorted_indices
HAN
бумага:Woohoo.ACL Web.org/anthology/N…
Код:GitHub.com/daily/special…
Все вышеприведенные классификации относятся к уровням предложений. Хотя также возможны длинные тексты и классификации на уровне глав, скорость и точность будут снижаться. Поэтому некоторые исследователи предложили иерархическую структуру классификации внимания, а именноHierarchical Attention
.
Вся структура сети включает пять частей:
-
кодировщик последовательности слов
-
слой внимания на уровне слов
-
Кодировщик предложений
-
Слой внимания на уровне предложения
-
Классификация
Вся сетевая структура состоит из двустороннихGRU
Механизм сети и внимания объединен.
кодировщик порядка слов
дано слово в предложении,возначает первыйприговор,означает первыйслова. Через матрицу встраивания словПреобразуйте слова в векторные представления следующим образом:
Введите полученный вектор слов в кодировщик слов, т.е.GRU
, два направленияGRU
Выходы объединяются вместе для получения скрытых векторов на уровне слов.
внимание на уровне слов
Но для слов в предложении не каждое слово полезно для задач классификации, например, при классификации текстов по настроению мы можем уделять больше внимания словам «очень хорошо» и «грустно». Чтобы позволить рекуррентной нейронной сети автоматически «сосредоточиться» на этих словах, автор разработал специальный процесс слоя внимания на основе слов следующим образом:
В приведенной выше формуледаПредставление скрытого слоя ,является поsoftmax
Нормализованный весовой коэффициент после обработки функции,представляет собой случайно инициализированный вектор, который затем обучается как параметр модели,это то, что мы получаемВекторное представление предложения.
Кодировщики предложений и внимание на уровне предложений
Для векторов уровня предложения мы используем аналогичныйGRU
и слой внимания, и, наконец, взвешенное суммирование скрытых векторных представлений всех предложений в документе для получения вектора документа всего документа., вектор классифицируется полносвязным классификатором.
основной код
class HierAttNet(nn.Module):
def __init__(self, rnn_type, word_hidden_size, sent_hidden_size, num_classes, word_embedding,
n_layers, bidirectional, batch_first, freeze, dropout):
super(HierAttNet, self).__init__()
self.word_embedding = word_embedding
self.word_hidden_size = word_hidden_size
self.sent_hidden_size = sent_hidden_size
self.word_att_net = WordAttNet(rnn_type,word_embedding, word_hidden_size,n_layers,bidirectional,batch_first,dropout,freeze)
self.sent_att_net = SentAttNet(rnn_type,sent_hidden_size, word_hidden_size,n_layers,bidirectional,batch_first,dropout, num_classes)
def forward(self, batch_doc, text_lengths):
output_list = []
# ############################ 词级 #########################################
for idx,doc in enumerate(batch_doc):
# 把一篇文档拆成多个句子
doc = doc[:text_lengths[idx]]
doc_list = doc.cpu().numpy().tolist()
sep_index = [i for i, num in enumerate(doc_list) if num == self.word_embedding.stoi['[SEP]']]
sentence_list = []
if sep_index:
pre = 0
for cur in sep_index:
sentence_list.append(doc_list[pre:cur])
pre = cur
sentence_list.append(doc_list[cur:])
else:
sentence_list.append(doc_list)
max_sentence_len = len(max(sentence_list,key=lambda x:len(x)))
seq_lens = []
input_token_ids = []
for sent in sentence_list:
cur_sent_len = len(sent)
seq_lens.append(cur_sent_len)
input_token_ids.append(sent+[self.word_embedding.stoi['PAD']]*(max_sentence_len-cur_sent_len))
input_token_ids = torch.LongTensor(np.array(input_token_ids)).to(batch_doc.device)
seq_lens = torch.LongTensor(np.array(seq_lens)).to(batch_doc.device)
word_output, hidden = self.word_att_net(input_token_ids,seq_lens)
# word_output = [bs,hidden_size]
output_list.append(word_output)
max_doc_sent_num = len(max(output_list,key=lambda x: len(x)))
batch_sent_lens = []
batch_sent_inputs = []
# ############################ 句子级 #########################################
for doc in output_list:
cur_doc_sent_len = len(doc)
batch_sent_lens.append(cur_doc_sent_len)
expand_doc = torch.cat([doc,torch.zeros(size=((max_doc_sent_num-cur_doc_sent_len),len(doc[0]))).to(doc.device)],dim=0)
batch_sent_inputs.append(expand_doc.unsqueeze(dim=0))
batch_sent_inputs = torch.cat(batch_sent_inputs, 0)
batch_sent_lens = torch.LongTensor(np.array(batch_sent_lens)).to(doc.device)
output = self.sent_att_net(batch_sent_inputs,batch_sent_lens)
return output
BERT
BERT(Bidirectional Encoder Representations from Transformers)
РелизNLP
Это событие стало одной из последних вех в развитии отрасли.NLP
Начало новой эры.BERT
Модель побила несколько рекордов для задач, связанных с обработкой языка. существуетBERT
Вскоре после публикации статьи команда также обнародовала код моделей и сделала доступными для загрузки модели, которые были предварительно обучены на крупномасштабных наборах данных. Это важное событие, поскольку оно позволяет любому, кто создает модель машинного обучения для обработки языка, использовать эту мощную функцию в качестве готового компонента, избавляя от необходимости обучать модель обработки языка с нуля, время, энергию, знания и ресурсы.
Подробнее см.【Графический BERT】,[Иллюстрированная модель BERT]
Task 1: Masked Language Model
так какBERT
Предсказывать информацию центрального слова необходимо через контекстную информацию, и в то же время не ожидается, что модель увидит информацию центрального слова заранее, поэтому предлагается новый метод.Masked Language Model
Метод предварительного обучения , то есть случайное предсказание по входным даннымmask
Отбросьте несколько слов, а затем предскажите слово по контексту, как в задаче на закрытие.
В предтренировочном задании 15%Word Piece
Будетmask
, 15%Word Piece
, в 80% случаев он будет напрямую заменен на[Mask]
, замените его любым другим словом в 10% случаев и сохраните исходное слово в 10% случаевToken
- нет 100%
mask
причина- если одно из предложений
Token
100% будетmask
выкл, затем вfine-tuning
Когда в модели появятся невидимые слова
- если одно из предложений
- Присоединяйтесь к 10% случайным
token
причина-
Transformer
Сохранить для каждого входаtoken
распределенное представление , иначе модель запомнила бы это[mask]
является конкретнымtoken
- Кроме того, кодер не знает, какие слова нужно предсказать, а какие нет, поэтому он вынужден запоминать каждое.
token
вектор представления
-
- Кроме того, каждый
batchsize
Только 15% слов былиmask
Причина в том, что из-за снижения производительности двунаправленный кодировщик медленнее обучается, чем кодировщик с одним элементом.
Task 2: Next Sequence Prediction
только одинMLM
задача не достаточно, чтобы сделатьBERT
Для решения задач на оценку отношения предложений, таких как понимание прочитанного, добавляется дополнительная предтренировочная задача, а именноNext Sequence Prediction
.
Конкретная задача представляет собой задачу суждения об отношениях между предложениями, то есть определить, является ли предложение B следующим предложением предложения A, и если да, то вывести результат.IsNext
, иначе выводNotNext
.
Обучающие данные генерируются путем случайного извлечения двух последовательных предложений из параллельного корпуса, 50% которых зарезервированы для двух извлеченных предложений, которые удовлетворяют требованиям.IsNext
отношения, остальные 50% вторых предложений случайным образом взяты из ожиданий, и их отношенияNotNext
из. Это отношение хранится в[CLS]
символ
входить
-
Token Embeddings
: Традиционный векторный слой слов, первый символ каждого входного образца должен быть установлен на[CLS]
, который можно использовать для последующих задач классификации.Если есть два разных предложения, нужно использовать[SEP]
разделены, и последний символ должен быть[SEP]
указывает на прекращение -
Segment Embeddings
:за[0,1]
последовательность, используемая вNSP
Различать два предложения в задании, что удобно для задания на оценку отношений между предложениями. -
Position Embeddings
:иTransformer
Векторы положения в различны,BERT
Вектор положения в непосредственно обучается
Fine-tunninng
Для различных последующих задач нам нужно толькоBERT
Выходные данные различных позиций могут быть обработаны, или выходные данные различных позиций BERT могут быть непосредственно введены в нижестоящую модель. В частности, следующим образом:
- Для задач классификации одного предложения, таких как анализ настроений, вы можете напрямую ввести одно предложение (нет необходимости
[SEP]
отдельные двойные предложения),[CLS]
Вывод напрямую вводится в классификатор для классификации - Для задачи на пару предложений (задача оценки отношения предложений) вам необходимо использовать
[SEP]
разделить два предложения на модель, а затем снова нужно только[CLS]
Результат отправляется в классификатор для классификации - Для заданий на ответы на вопросы вопрос и ответ объединяются в
BERT
В модели выходной вектор позиции ответа затем бинаризуется и обрабатывается в направлении предложения.softmax
(просто предскажите начальную и конечную позиции) - Для задач распознавания именованных сущностей достаточно классифицировать выходные данные каждой позиции.Если выходные данные каждой позиции
CRF
добьется лучших результатов.
недостаток
-
BERT
предтренировочные задачиMLM
Он позволяет кодировать последовательность с помощью контекста, но в то же время делает так, что данные в процессе предобучения не совпадают с данными в тонкой настройке, что затрудняет адаптацию к генеративным задачам. - Кроме того, BERT не учитывает прогнозы.
[MASK]
Корреляция между является необъективной оценкой совместной вероятности языковой модели - Из-за ограничения максимальной длины ввода подходит для задач на уровне предложения и абзаца, но не подходит для задач на уровне документа (например, для классификации длинного текста).
- Подходит для решения задач естественного семантического понимания (
NLU
), не подходит для задач генерации естественного языка (NLG
)
BERT
Оптимизацию классификации можно попробовать:
-
Попробуйте разные предварительно обученные модели, такие как
RoBERT
,WWM
,ALBERT
-
Кроме
[CLS]
также можно использоватьavg
,max
Объединение используется для представления предложений, и даже разные слои могут быть объединены.
Инкрементная предварительная подготовка данных домена
-
Интегрируйте дистилляцию, обучите несколько больших моделей и объедините их в одну
-
Сначала используйте многозадачное обучение, а затем переходите к своим собственным задачам
основной код
class BertForSeqCLS(nn.Module):
def __init__(self, config, train=True):
super(BertForSeqCLS, self).__init__()
self.bert = BertModel.from_pretrained(config.bert_path)
# 对bert进行训练
for param in self.bert.parameters():
param.requires_grad = train
self.dropout = nn.Dropout(config.dropout)
self.fc = nn.Linear(768 * 3, config.class_num)
def forward(self, input_ids, attention_mask, labels=None):
# input_ids 输入的句子序列
# seq_len 句子长度
# attention_masks 对padding部分进行mask,和句子一个size,padding部分用0表示,如:[1, 1, 1, 1, 0, 0]
# pooled_out [batch_size, 768]
# sentence [batch size, sen len, 768]
outputs = self.bert(input_ids, attention_mask=attention_mask,
output_hidden_states=True)
cat_out = torch.cat((outputs.pooler_output, outputs.hidden_states[-1][:,0],
outputs.hidden_states[-2][:, 0]), 1)
logits = self.fc(self.dropout(cat_out))
loss = None
if labels is not None:
loss = F.cross_entropy(logits.view(-1, config.class_num), labels.view(-1))
return {"loss": loss, "logits": logits}
Советы по классификации текста
построение набора данных
Во-первых, построение системы меток.Когда вы получаете задание, вы сначала пробуете одну или две сотни меток, чтобы увидеть, сколько из них трудно определить (подумайте больше, чем 1 с).Если пропорция слишком велика, то определение этой задачи будет проблематичным. Может случиться так, что система маркировки неясна или категории, которые нужно классифицировать, слишком сложны.В это время следует попросить владельца проекта дать отзыв, а не продолжать.
Во-вторых, построение обучающего оценочного набора.Можно построить два оценочных набора: один представляет собой набор онлайн-оценок, который соответствует распределению реальных данных, чтобы отразить онлайн-эффект, а другой представляет собой набор случайных оценок, который равномерно отбирается после дедупликация с помощью правил, соответствующих модели. Распределение обучающего набора максимально соответствует оценочному набору.Иногда мы будем переходить в аналогичное поле, чтобы получить готовые помеченные обучающие данные.В это время мы должны обратить внимание на настройку распределения, например длину предложения , пунктуация, чистота и т. д., и изо всех сил стараемся быть собой. Невозможно сказать, взято ли это предложение из этой задачи или заимствовано у кого-то другого.
Наконец, очистка данных:
-
Убрать сильные текстовые шаблоны:Например, в классификации новостных тем бесполезны отчеты ХХ и редактирование ХХ высокочастотных полей в каких-то облазивших данных.Можно посчитать фрагменты или слова корпуса и убрать бесполезное элементы с высокой частотой. Есть также некоторые, которые, очевидно, повлияют на оценку модели.Например, при оценке того, является ли предложение бессмысленной болтовней, обнаруживается, что добавление точки превратит выборку из положительной в отрицательную, потому что небольшая болтовня, ожидаемая обучение редко имеет период (у всех привычка печатать), поэтому удаление этого шаблона намного лучше
-
Исправьте ошибки маркировки: просто соберите обучающий набор и оценочный набор, используйте набор данных для обучения модели в течение двух или трех эпох (для предотвращения переобучения), затем предскажите набор данных, удалите неправильную модель и нажмите abs(label-prob) Сортировка, если будет меньше, то вы сами увидите, а если больше, то скормите маркировщикам.Возможно улучшить качество данных на несколько пунктов.
длинный текст
Если задача простая (например, классификация новостей), используйте ее напрямуюfasttext
Вы можете добиться хороших результатов.
хочу использоватьBERT
Самый простой способ — грубо обрезать, например, взять всего несколько слов из начала предложения + конец предложения, начало предложения + сито tfidf; или предсказать каждое предложение и, наконец, обобщить результаты.
Кроме того, есть несколько модифицированных магией моделей, которые вы можете попробовать, напримерBERT+HAN
,XLNet
,Reformer
,Longformer
.
прочность
В практических приложениях надежность является очень важным вопросом, иначе перед лицомbadcase
Временами будет очень неловко, как можно так четко разделить, а добавить слово неправильно?
Здесь вы можете напрямую использовать некоторые грубые улучшения данных, добавлять стоп-слова, добавлять знаки препинания, удалять слова, заменять синонимы и т. д. Если эффект уменьшается, стирайте расширенные обучающие данные.
Конечно, вы также можете использовать навыки высокого уровня, такие как состязательное обучение и контрастное обучение для улучшения.Как правило, вы можете поднять примерно 1 балл, но, возможно, вам не удастся избежать вышеуказанной неловкой ситуации.
Ссылка на ссылку
- АР Вест V.org/ABS/1607.01…
- АР Вест V.org/ABS/1408.58…
- Love.Tencent.com/Arab/Media…
- Войдите на .ACM.org/do i/10.5555…
- Woohoo.ACL Web.org/anthology/N…
- zhuanlan.zhihu.com/p/266364526
- cloud.Tencent.com/developer/ ах…
- Блог woohoo.cn на.com/sandwich НЛП…
- GitHub.com/Джефф 0628…
- zhuanlan.zhihu.com/p/349086747
- zhuanlan.zhihu.com/p/35457093
- Ууху. Deeper.com/article/431…
- Ууху. Call.com/question/32…