Путеводитель по LLM

  • Руководство по LLM
  • Python & LLM для анализа рынка
  • Создание LLM с использованием Python

Руководство по LLM

Генеративный искусственный интеллект (GenAI), особенно ChatGPT, привлекает всеобщее внимание. Большие языковые модели (LLM) на основе трансформеров, обученные на огромном количестве неразмеченных данных в большом масштабе, демонстрируют способность обобщать их для множества различных задач. Чтобы понять, почему LLM так сильны, мы подробно рассмотрим, как они работают, в этом посте.

Формально языковая модель, состоящая только из декодера, является просто условным распределением p(xi|x1···xi−1)−1) над следующими лексемами xi с заданными контекстами x1 · · · xi−1. Такая формулировка является примером марковского процесса, который был изучен во многих случаях использования. Эта простая настройка также позволяет нам генерировать токен за токеном в авторегрессионном режиме.

Прежде чем углубиться в эту тему, я должен указать на ограниченность этой формулировки для достижения общего искусственного интеллекта (AGI). Мышление – процесс нелинейный, но наш коммуникативный аппарат – рот – может говорить только линейно. Поэтому язык предстает линейной последовательностью слов. Это разумное начало для моделирования языка с помощью марковского процесса. Но я подозреваю, что эта формулировка может полностью охватить мыслительный процесс (или AGI). С другой стороны, мышление и язык взаимосвязаны. Достаточно сильная языковая модель все еще может демонстрировать какие-то мыслительные способности, как показывает GPT4. Далее давайте рассмотрим научные инновации, благодаря которым LLM появляются разумно.

Трансформатор

Существует множество способов моделирования/представления условного распределения p(xi|x1···xi−1). В LLM мы пытаемся оценить это условное распределение с помощью архитектуры нейронной сети под названием Transformer. На самом деле, нейронные сети, особенно разновидность рекуррентных нейронных сетей (РНС), использовались в языковом моделировании задолго до Transformer. RNN обрабатывают маркеры последовательно, поддерживая вектор состояния, содержащий представление данных, видимых до текущего маркера. Для обработки n-го маркера модель объединяет состояние, представляющее предложение до маркера n-1, со сведениями нового маркера, чтобы создать новое состояние, представляющее предложение до маркера n.n Теоретически информация из одной лексемы может распространяться сколь угодно далеко вниз по последовательности, если в каждой точке состояние продолжает кодировать контекстную информацию о лексеме. К сожалению, проблема исчезающего градиента оставляет состояние модели в конце длинного предложения без точной, извлекаемой информации о предшествующих лексемах. Зависимость вычислений токенов от результатов предыдущих вычислений токенов также затрудняет распараллеливание вычислений на современном оборудовании графического процессора.

Эти проблемы были решены с помощью механизмов самовнимания в «Трансформере». Трансформер — это модельная архитектура, избегающая повторения и вместо этого полностью полагающаяся на механизм внимания для выявления глобальных зависимостей между входными и выходными данными. Уровень внимания может получить доступ ко всем предыдущим состояниям и взвесить их в соответствии с изученной мерой релевантности, предоставляя релевантную информацию о далеких токенах. Важно отметить, что трансформеры используют механизм внимания без RNN, обрабатывая все токены одновременно и вычисляя веса внимания между ними в последовательных слоях. Поскольку механизм внимания использует информацию только о других токенах с более низких уровней, его можно вычислить для всех токенов параллельно, что приводит к повышению скорости обучения.

Входной текст разбирается на маркеры с помощью генератора маркеров байтовой пары, и каждый маркер преобразуется в вектор внедрения. Затем к встраиванию добавляется информация о местоположении токена. Строительные блоки трансформатора представляют собой масштабируемые блоки внимания на основе точечного произведения. Когда предложение передается в модель-трансформер, весовые коэффициенты внимания вычисляются между каждой лексемой одновременно. Блок внимания создает внедрения для каждого маркера в контексте, которые содержат сведения о самом маркере, а также взвешенную комбинацию других релевантных маркеров, каждый из которых взвешен по весу внимания.

Для каждой единицы внимания модель трансформера изучает три весовые матрицы; вес запроса — WQ, вес ключа — WK, а вес значения — WV. Для каждой лексемы i вложение входного слова умножается на каждую из трех матриц весов, чтобы получить вектор запроса qi, ключевой вектор ki и вектор значений vi. Весовые коэффициенты внимания представляют собой скалярное произведение между qi и kj, масштабированное по квадратному корню из размерности ключевых векторов и нормализованное через softmax. Выход единицы внимания для токена i представляет собой взвешенную сумму векторов значений всех токенов, взвешенную по вниманию от токена i к каждому токену j. Вычисление внимания для всех токенов можно выразить в виде одного большого матричного вычисления:

Один набор матриц (WQ, WK, WV) называется головкой внимания, и каждый слой трансформатора имеет несколько головок внимания. При наличии нескольких головок внимания модель может вычислять различную релевантность между токенами. Вычисления для каждой головы внимания могут выполняться параллельно, а выходные данные объединяются и проецируются обратно в одно и то же входное измерение с помощью матрицы WO.

В энкодере после механизма самовнимания находится полносвязный многослойный персептрон (MLP). Далее блок MLP обрабатывает каждую выходную кодировку по отдельности. В настройках энкодера-декодера (например, для трансляции) между самовниманием и MLP в декодер вставляется дополнительный механизм внимания, чтобы извлечь соответствующую информацию из кодировок, сгенерированных энкодерами. В архитектуре, состоящей только из декодера, в этом нет необходимости. Независимо от архитектуры кодера-декодера или только декодера, декодер не должен использовать текущий или будущий выход для прогнозирования выхода, поэтому выходная последовательность должна быть частично замаскирована, чтобы предотвратить этот обратный поток информации, что позволяет генерировать авторегрессионный текст. Для генерации токена за токеном за последним декодером следует слой softmax для получения выходных вероятностей по словарю.

Контролируемая тонкая настройка

GPT только для декодера — это, по сути, неконтролируемый (или самоконтролируемый) алгоритм предварительного обучения, который максимизирует следующую вероятность:

где k — размер контекстного окна. Несмотря на то, что архитектура не зависит от задач, GPT демонстрирует, что большие преимущества в выводе на естественном языке, ответах на вопросы, семантическом сходстве и классификации текста могут быть реализованы за счет генеративного предварительного обучения языковой модели на разнообразном корпусе неразмеченного текста с последующей дискриминационной тонкой настройкой для каждой конкретной задачи.

После предварительного обучения модели с вышеуказанной целью мы можем адаптировать параметры к контролируемой целевой задаче. Дан помеченный набор данных C, где каждый экземпляр состоит из последовательности входных маркеров, x1, …, xm, а также метки y. Входные данные пропускаются через предварительно обученную модель для получения активации конечного блока трансформатора hlm, который затем подается в дополнительный линейный выходной слой с параметрами Wy для предсказания y:

Соответственно, мы имеем следующую целевую функцию:

Кроме того, полезно включить языковое моделирование в качестве вспомогательной цели, так как это улучшает обобщение контролируемой модели и ускоряет сходимость. То есть оптимизируем следующую цель:

Классификацию текста можно настроить напрямую, как описано выше. Другие задачи, такие как ответы на вопросы или текстовые следствия, имеют структурированные входные данные, такие как упорядоченные пары предложений или триплеты из документа, вопроса и ответов. Поскольку предварительно обученная модель была обучена на непрерывных последовательностях текста, для применения к этим задачам необходимо внести некоторые изменения.

Текстовое следование: объедините последовательности маркеров посылки p и гипотезы h с маркером-разделителем ($) между ними.
Сходство: нет внутреннего порядка двух сравниваемых предложений. Таким образом, входная последовательность содержит оба возможных порядка предложений (с разделителем между ними) и обрабатывает каждое независимо для получения двух представлений последовательности, которые добавляются поэлементно перед подачей в линейный выходной слой.
Ответы на вопросы и рассуждения на основе здравого смысла: каждый пример имеет контекстный документ z, вопрос q и набор возможных ответов {ak}. GPT объединяет контекст документа и вопрос с каждым возможным ответом, добавляя маркер-разделитель между ними, чтобы получить [z; q;$; а.к. Каждая из этих последовательностей обрабатывается независимо друг от друга, а затем нормализуется с помощью слоя softmax для получения выходного распределения по возможным ответам.

Zero-Shot Transfer (aka Meta Learning)

Хотя GPT показывает, что контролируемая тонкая настройка хорошо работает с наборами данных для конкретных задач, для достижения высокой производительности в желаемой задаче обычно требуется тонкая настройка набора данных из тысяч или сотен тысяч примеров, специфичных для этой задачи. Интересно, что GPT2 демонстрирует, что языковые модели начинают изучать несколько задач без какого-либо явного контроля, обусловленного документом и вопросами (также известными как подсказки).

Обучение выполнению одной задачи может быть выражено в вероятностной структуре как оценка условного распределения p(output|input) Поскольку общая система должна быть способна выполнять множество различных задач даже для одного и того же входа, она должна зависеть не только от входных данных, но и от задачи, которую необходимо выполнить. То есть, он должен моделировать p(output|input, task). Ранее обусловливание задач часто реализовывалось на архитектурном уровне или на алгоритмическом уровне. Но язык предоставляет гибкий способ определения задач, входных и выходных данных в виде последовательности символов. Например, пример обучения переводу может быть записан в виде последовательности (translate to french, english text, french text) В частности, GPT2 обусловливается контекстом примеров пар формата english sentence = French sentence предложение, а затем после финальной подсказки english sentence = мы выбираем из модели с жадным декодированием и используем первое сгенерированное предложение в качестве перевода.

Аналогичным образом, чтобы вызвать поведение суммирования, GPT2 добавляет текст TL;DR: после статьи и сгенерировать 100 токенов с Top-k случайной выборкой с k = 2, что уменьшает повторения и поощряет более абстрактные резюме, чем жадное декодирование. Точно так же пример тренировки понимания прочитанного можно записать как (ответ на вопрос, (answer the question, document, question, answer).

Обратите внимание, что перенос с нуля отличается от обучения с нулевым выстрелом в следующем разделе. В передаче с нулевым выстрелом «нулевой выстрел» означает, что не выполняется обновление градиента, но он часто включает в себя предоставление демонстрации модели во время вывода (например, в приведенном выше примере перевода), поэтому на самом деле не обучается на нулевых примерах.

Я нахожу интересную связь между этим подходом к метаобучению с семантикой Монтегю, которая является теорией семантики естественного языка, и ее взаимосвязью с синтаксисом. В 1970 году Монтегю сформулировал свои взгляды:

На мой взгляд, между естественными языками и искусственными языками логиков нет существенного теоретического различия; более того, я считаю возможным понять синтаксис и семантику обоих видов языков с помощью единой естественной и математически точной теории.

С философской точки зрения, как перенос с нуля, так и семантика Монтегю относятся к естественному языку так же, как и к языку программирования. LLM фиксируют задачу с помощью подхода «вложение векторов в черный ящик». Но нам непонятно, как это работает на самом деле. Напротив, важнейшей чертой семантики Монтекки является ее приверженность принципу композиционности, т. е. значение целого является функцией значений его частей и способа их синтаксического сочетания. Это может быть подходом к улучшению LLM.

Контекстное обучение

GPT3 показывает, что масштабирование языковых моделей значительно повышает производительность, не зависящую от задач. Кроме того, GPT3 специализируется на описании на «нулевой выстрел», «однократный» или «несколько» в зависимости от того, сколько демонстраций предоставляется во время вывода: (а) «обучение с несколькими выстрелами» или обучение в контексте, когда мы разрешаем столько демонстраций, сколько поместится в контекстное окно модели (обычно от 10 до 100), (б) «обучение с одного выстрела», когда мы разрешаем только одну демонстрацию, и © обучение с нулевым выстрелом, где демонстрация не допускается, а модели дается только инструкция на естественном языке.

Для обучения с небольшим количеством выстрелов GPT3 оценивает каждый пример в наборе оценки, случайным образом вытягивая K примеров из обучающего набора этой задачи в качестве обусловления, разделенных 1 или 2 новыми строками в зависимости от задачи. K может быть любым значением от 0 до максимального значения, разрешенного контекстным окном модели, которое составляет nctx = 2048 для всех моделей и обычно подходит для 10-100 примеров. Большие значения K обычно, но не всегда, лучше.

Для некоторых задач GPT3 также использует приглашение на естественном языке в дополнение к демонстрациям (или для K = 0, вместо). В задачах, включающих выбор одного правильного завершения из нескольких вариантов (множественный выбор), запрос включает в себя K примеров контекста плюс правильное завершение, за которыми следует только один пример контекста, и процесс оценки сравнивает вероятность модели каждого завершения.

В задачах, включающих двоичную классификацию, GPT3 дает вариантам более семантически значимые имена (например, «Истина» или «Ложь», а не 0 или 1), а затем обрабатывает задачу как множественный выбор.

В задачах с завершением в произвольной форме GPT3 использует поиск по лучу. В процессе оценки модель оценивается с помощью оценки сходства F1, BLEU или точного совпадения, в зависимости от того, что является стандартным для имеющегося набора данных.

Размер модели имеет значение (пока)

Производительность языковой модели имеет важное значение для успеха обучения, не зависящего от задачи, и ее увеличение повышает производительность логарифмически линейно между задачами. GPT-2 был создан как прямое масштабирование GPT-1, при этом количество параметров и размер набора данных были увеличены в 10 раз. Но он может выполнять последующие задачи в условиях передачи данных с нулевым выбросом — без каких-либо изменений параметров или архитектуры.

GPT3 использует ту же модель и архитектуру, что и GPT2, за исключением использования чередующихся плотных и локально полосчатых разреженных паттернов внимания в слоях трансформатора.

Размер модели

В TriviaQA производительность GPT3 плавно растет с размером модели, что говорит о том, что языковые модели продолжают поглощать знания по мере увеличения их емкости. Эффективность одного выстрела и нескольких выстрелов значительно превосходит поведение с нулевым выстрелом.

Качество данных имеет значение

Хотя это и не обсуждается, качество данных также имеет значение. Наборы данных для языковых моделей быстро расширяются. Например, набор данных CommonCrawl содержит почти триллион слов, чего достаточно для обучения самых больших моделей без повторного обновления одной и той же последовательности. Тем не менее, было обнаружено, что нефильтрованные или слегка отфильтрованные версии CommonCrawl, как правило, имеют более низкое качество, чем более тщательно отобранные наборы данных.

Поэтому GPT2 создал новый веб-скрейпинг, который делает акцент на качестве документа, очищая все исходящие ссылки с Reddit, получившие не менее 3 кармы, что действует как эвристический индикатор того, считают ли другие пользователи ссылку интересной, образовательной или просто забавной. Окончательный набор данных содержит чуть более 8 миллионов документов, что составляет в общей сложности 40 ГБ текста после дедупликации и некоторой эвристической очистки.

Кроме того, GPT3 предпринял 3 шага для улучшения среднего качества наборов данных: (1) отфильтровал CommonCrawl на основе сходства с диапазоном высококачественных эталонных корпусов, (2) нечеткую дедупликацию на уровне документа, внутри и между наборами данных, чтобы предотвратить избыточность и сохранить целостность удерживаемого валидационного набора в качестве точной меры переобучения, и (3) добавил известные высококачественные эталонные корпуса в набор для обучения, чтобы дополнить CommonCrawl и увеличить его разнообразие.

Аналогичным образом, GLaM разрабатывает классификатор качества текста для создания высококачественного веб-корпуса из исходного большого необработанного корпуса. Этот классификатор обучен классифицировать коллекцию курируемых текстов (Википедия, книги и несколько избранных веб-сайтов) и другие веб-страницы. GLaM использует этот классификатор для оценки качества содержимого веб-страницы, а затем использует распределение Парето для выборки веб-страниц в соответствии с их оценкой. Это позволяет включать некоторые веб-страницы более низкого качества, чтобы предотвратить систематические ошибки в классификаторе.

Данные и весовые коэффициенты смесей в обучающем наборе GLaM

GLaM также устанавливает веса смеси в зависимости от производительности каждого компонента данных в меньшей модели и для предотвращения избыточной выборки небольших источников, таких как Википедия.

Цепочка мыслей

Как указывалось ранее, предсказание следующего токена — это не то же самое, что мыслительный процесс. Интересно, что некоторые способности к рассуждению и арифметике магистров права могут быть разблокированы с помощью подсказки Chain of Thought-ofЦепочка мыслей — это последовательность промежуточных шагов рассуждений на естественном языке, которые приводят к конечному результату. Достаточно большие языковые модели могут генерировать цепочки мыслей, если демонстрации цепочек рассуждений представлены в примерах для нескольких подсказок: ⟨вход, цепочка мыслей, выход⟩. Почему и как это работает, нам непонятно.

Обучение с подкреплением на основе обратной связи от человека (RLHF)

Цель языкового моделирования, используемая для LLM — прогнозирование следующего токена — отличается от цели «следовать инструкциям пользователя полезно и безопасно». Таким образом, мы говорим, что цель моделирования языка смещена.

InstructGPT согласовывает языковые модели с намерениями пользователя в широком спектре задач с помощью обучения с подкреплением из обратной связи с человеком (RLHF). Этот метод использует человеческие предпочтения в качестве сигнала вознаграждения для тонкой настройки моделей.

Шаг 1: Соберите демонстрационные данные и обучите контролируемую политику. Маркировщики демонстрируют желаемое поведение при распределении входных подсказок. Затем выполните тонкую настройку предварительно обученной модели GPT3 на основе этих данных с помощью контролируемого обучения.

Шаг 2: Соберите сравнительные данные и обучите модель вознаграждения. Соберите набор данных сравнений между выходными данными модели, где маркировщики указывают, какие выходные данные они предпочитают для заданных входных данных. Затем обучите модель вознаграждения для прогнозирования предпочтительного для человека результата.

Шаг 3: Оптимизируйте политику в соответствии с моделью вознаграждения с помощью PPO. Используйте выходные данные RM в качестве скалярного вознаграждения. Выполните тонкую настройку контролируемой политики, чтобы оптимизировать это вознаграждение с помощью алгоритма PPO.

Шаги 2 и 3 можно повторять непрерывно; Дополнительные сравнительные данные собираются по текущей лучшей политике, которая используется для обучения нового RM, а затем новой политики.

Тонкая настройка инструкций

В то время как контролируемая тонкая настройка, представленная в GPT-1, фокусируется на настройке конкретной задачи, T5 обучается с целью максимального правдоподобия (с использованием «принуждения учителя») независимо от задачи. По сути, Т5 использует ту же интуицию, что и перенос нулевого выстрела, что задачи НЛП могут быть описаны с помощью инструкций на естественном языке, таких как «Настроение рецензии на этот фильм положительное или отрицательное?» или «Переведите «как дела» на китайский». Чтобы указать, какую задачу должна выполнять модель, T5 добавляет текстовый префикс к исходной входной последовательности перед передачей ее в модель. Кроме того, FLAN исследует тонкую настройку инструкций, уделяя особое внимание (1) масштабированию количества задач, (2) масштабированию размера модели и (3) тонкой настройке данных цепочки мыслей.

Для каждого набора данных FLAN вручную составляет десять уникальных шаблонов, которые используют инструкции на естественном языке для описания задачи для этого набора данных. В то время как большинство из десяти шаблонов описывают исходную задачу, для увеличения разнообразия для каждого набора данных FLAN также включает до трех шаблонов, которые «перевернули задачу» (например, для классификации тональности мы включаем шаблоны, запрашивающие рецензию на фильм). Затем мы настраиваем предварительно обученную языковую модель на смеси всех наборов данных, при этом примеры в каждом наборе данных форматируются с помощью случайно выбранного шаблона инструкций для этого набора данных.

Так называемый prompt engineering — это, по сути, реверс-инжиниринг того, как обучающие данные подготавливаются к тонкой настройке инструкций и контекстному обучению.

Извлечение дополненной генерации (RAG)

Из-за дороговизны и времени LLM в рабочей среде часто отстают с точки зрения актуальности обучающих данных. Чтобы решить эту проблему, мы можем использовать LLM в виде Retrieval Augmented Generation (RAG). В этом случае мы не хотим, чтобы LLM генерировал текст, основанный исключительно на данных, на которых он был обучен, а скорее хотим, чтобы он каким-то образом включал другие внешние данные. С помощью RAG магистры права также могут отвечать на вопросы (частные) по предметной области. Поэтому RAG также называют ответами на вопросы «открытой книги». LLM + RAG может стать альтернативой классическому поисковику. Другими словами, он действует как поиск информации с галлюцинациями.

В настоящее время поисковая часть RAG часто реализуется в виде поиска k-ближайшего соседа (подобия) в векторной базе данных, содержащей векторное вложение внешних текстовых данных. Например, DPR формулирует обучение кодировщика как задачу обучения метрикам. Однако следует отметить, что поиск информации, как правило, основан на релевантности, которая отличается от сходства. Я ожидаю, что в будущем будет еще много улучшений в этой области.

Заключение

LLM — это захватывающая область, в которой будут быстро внедряться инновации. Я надеюсь, что этот пост поможет вам немного понять, как это работает. Помимо волнения, мы также должны заметить, что магистры права изучают язык совсем не так, как люди — им не хватает доступа к социальному и перцептивному контексту, который изучающие человеческий язык используют, чтобы сделать вывод о взаимосвязи между высказываниями и психическими состояниями говорящих. Они также тренируются не так, как человеческий мыслительный процесс. Это могут быть области для улучшения LLM или для изобретения новых парадигм алгоритмов обучения.

Python & LLM для анализа рынка

Часть I. Технические индикаторы

Многие трейдеры включают технические стратегии наряду со своими фундаментальными подходами, пытаясь усовершенствовать время входа и выхода из рынка. Достижение безупречного тайминга на рынке может показаться труднодостижимым, но есть ценные индикаторы, которые могут значительно улучшить ваши решения о покупке и продаже.

В этом посте мы рассмотрим, как рассчитать основные технические индикаторы с помощью Python и Pandas, используя библиотеки TA (Technical Analysis). Несмотря на то, что существует множество индикаторов на выбор, я сосредоточусь на нескольких из моих личных фаворитов:

  1. RSI
  2. Экспоненциальная скользящая средняя (EMA)
  3. Каналы Кельтнера

Для начала вам понадобятся исторические данные. Хотя я предпочитаю использовать API Zerodha Kite для торговли в реальном времени и исторических данных, за него взимается абонентская плата. Кроме того, вы можете получить доступ к бесплатным данным через функцию GOOGLEFINANCE в Google Таблицах или использовать библиотеку jugaad_data на Python.

Вот основные данные, которые вам потребуются:

  • ДАТА
  • Открытый
  • Высокий
  • Низкий
  • Закрывать

В этом руководстве мы продемонстрируем использование библиотеки jugaad_data. Обратите внимание, что, несмотря на то, что эта библиотека предоставляет бесплатные данные, она может не поддерживаться активно или не быть полностью надежной. Для более надежных источников данных рассмотрите Google Finance или Zerodha Kite API.

Теперь давайте углубимся в процесс расчета этих важнейших технических индикаторов для улучшения вашей торговой стратегии.

  1. Установка библиотеки

pip install jugaad-data

2. Импорт необходимых библиотекfrom datetime import datetime, timedelta
import pandas as pd
from jugaad_data.nse import stock_df

3. Получение данных в Pandas DataFrame@staticmethod
def extract_jugaad_data(tick,start,end):
df1 = stock_df(symbol=tick, from_date=start,
to_date=end, series=»EQ»)
return df1

Описанный выше метод требует, чтобы мы отправили название акции, а также даты начала и окончания. Этот метод всегда будет возвращать исторические данные за день. для большего количества вариантов, таких как 5 м, 10 м, 1 ч, 1 Вт и т. Д., Используйте API воздушного змея Zerodha.

4. Допустим, мы хотим извлечь исторические данные за 5 лет. Мы должны итеративно извлекать данные за каждый год. Запуск большого диапазона дат может привести к проблемам с производительностью и задержкам, а также к сбоям. Следовательно, нам нужен этот итеративный подход.class HistoricalData:
@staticmethod
def calculate_timelines(years):
curr = datetime.now().date()
ans = []
while years>0:
start = curr- timedelta(days=365)
ans.insert(0,(start,curr))
curr=start
years-=1
return ans

Приведенный выше метод вернет список дат начала и окончания.

5. Теперь, чтобы получить исторические данные, давайте сделаем это ниже def get_history_df(tick):
df = pd.DataFrame({})
for start,end in HistoricalData.calculate_timelines(5):
df = pd.concat([df,HistoricalData.extract_jugaad_data(tick,start,end)],ignore_index=True)
df = df[[«DATE», «OPEN», «HIGH»,»LOW»,»CLOSE»,»VOLUME»,»SYMBOL»]]
df[«DATE»] = pd.to_datetime(df[«DATE»], format=’%Y-%m-%d’)
df.set_index(‘DATE’,inplace=True)
df = df[~df.index.duplicated(keep=’first’)]
df = df.sort_values(by=’DATE’)

Здесь мы перебираем исторические данные, начальную и конечную даты и связываем данные с фреймом данных. затем используйте выбранные столбцы и преобразуйте столбец DATE в поле даты и времени pandas. Выполните очистку данных, удалив дубликаты и отсортировав данные по полю ДАТА. Убедитесь, что индекс установлен как дата, так как в каждой дате должна быть только одна строка.

6. Давайте коснемся самой важной части этого поста, расчета технических индикаторов. Здесь мы используем 2 библиотекиpip install ta
pip install TA-Lib

7. Обе библиотеки поддерживают широкий спектр технических индикаторов. Давайте попробуем некоторые из них. Давайте создадим файл tech_analysis.pyimport pandas as pd
import numpy as np
from talib import RSI, EMA, stream
from ta.volatility import KeltnerChannel
class TechAnalysis:
def __init__(self):
self.ema_period = 13
self.rsi_period = 7
self.stochastic_period = 7
self.rsi_divergence_period = 14

def calculate_technicals(self, df):
try:
df[‘EMA’] = EMA(df[‘CLOSE’],timeperiod=self.ema_period)
df[‘RSI7’] = RSI(df[‘EMA’], timeperiod=self.rsi_period)
df[‘RSI14’] = RSI(df[‘CLOSE’], timeperiod=self.rsi_divergence_period)
indicator_kc = KeltnerChannel(high=df[«HIGH»],low=df[«LOW»],close=df[«CLOSE»], window=50, window_atr=50,fillna=False,multiplier=5, original_version=False)
df[«KC_UPPER»] = indicator_kc.keltner_channel_hband()
df[«KC_MIDDLE»] = indicator_kc.keltner_channel_mband()
df[«KC_LOWER»] = indicator_kc.keltner_channel_lband()
return df
except Exception as e:
print(‘error while calculating technicals’)
print(f’exception occured {e}’)

TA = TechAnalysis()

Обратите внимание, что мы используем EMA для RSI7, но CLOSE для RSI14. Существуют различные способы расчета индикаторов RSI, и каждый из них имеет свои плюсы и минусы. EMA(),RSI() являются частью talib, а KeltnerChannel() — частью ta.volatility.

Канал Кельтнера имеет 3 полосы, верхнюю, среднюю и нижнюю. Чтобы его вычислить, инициируйте канал Кельтнераindicator_kc = KeltnerChannel(high=df[«HIGH»],low=df[«LOW»],
close=df[«CLOSE»], window=50, window_atr=50,fillna=False,multiplier=5,
original_version=False)

Мы используем 50-дневное окно с 5 в качестве множителя. Обратите внимание, что канал Кельтнера требует высоких и низких значений.

Затем используйте indicator_kc для вычисления полос. Например, для вычисления верхней полосы indicator_kc.keltner_channel_hband(). Я рекомендую выставить другие методы этих 2 библиотек.

Вот ссылка на TA-Lib и ссылка на .

Возвращает кадр данных, чтобы столбцы были добавлены во фрейм данных.

8. Теперь обновим наш get_history_df (тик) из нашего шага 5 для расчета технических индикаторов. Добавьте импорт вверху и сохраните данные в CSV.from tech_analysis import TAdef get_history_df(tick):
df = pd.DataFrame({})
for start,end in HistoricalData.calculate_timelines(5):
df = pd.concat([df,HistoricalData.extract_jugaad_data(tick,start,end)],ignore_index=True)
df = df[[«DATE», «OPEN», «HIGH»,»LOW»,»CLOSE»,»VOLUME»,»SYMBOL»]]
df[«DATE»] = pd.to_datetime(df[«DATE»], format=’%Y-%m-%d’)
df.set_index(‘DATE’,inplace=True)
df = df[~df.index.duplicated(keep=’first’)]
df = df.sort_values(by=’DATE’)
df = TA.calculate_technicals(df)
df.to_csv(f’./{tick}.csv’)

Приведенный выше код теперь будет хранить CSV-файл в той же папке, где находится код.

9. В целом, то, что мы видели до сих пор, можно свести воедино, как показано ниже. Запустите technical_data.py и обратите внимание, что csv-файл сгенерирован со значениями технических индикаторов

technicals_data.pyfrom datetime import datetime, timedelta
import pandas as pd
from jugaad_data.nse import stock_df
from tech_analysis import TA
class HistoricalData:
@staticmethod
def calculate_timelines(years):
curr = datetime.now().date()
ans = []
while years>0:
start = curr- timedelta(days=365)
ans.insert(0,(start,curr))
curr=start
years-=1
return ans

@staticmethod
def extract_jugaad_data(tick,start,end):
df1 = stock_df(symbol=tick, from_date=start,
to_date=end, series=»EQ»)
return df1

@staticmethod
def get_history_df(tick):
df = pd.DataFrame({})
for start,end in HistoricalData.calculate_timelines(5):
df = pd.concat([df,HistoricalData.extract_jugaad_data(tick,start,end)],ignore_index=True)
df = df[[«DATE», «OPEN», «HIGH»,»LOW»,»CLOSE»,»VOLUME»,»SYMBOL»]]
df[«DATE»] = pd.to_datetime(df[«DATE»], format=’%Y-%m-%d’)
df.set_index(‘DATE’,inplace=True)
df = df[~df.index.duplicated(keep=’first’)]
df = df.sort_values(by=’DATE’)
df = TA.calculate_technicals(df)
df.to_csv(f’./{tick}.csv’)

HistoricalData.get_history_df(«INFY»)

tech_analysis.pyimport pandas as pd
import numpy as np
from talib import RSI, EMA, stream
from ta.volatility import KeltnerChannel
class TechAnalysis:
def __init__(self):
self.ema_period = 13
self.rsi_period = 7
self.stochastic_period = 7
self.rsi_divergence_period = 14

def calculate_technicals(self, df):
try:
df[‘EMA’] = EMA(df[‘CLOSE’],timeperiod=self.ema_period)
df[‘RSI’] = RSI(df[‘EMA’], timeperiod=self.rsi_period)
df[‘RSI14’] = RSI(df[‘CLOSE’], timeperiod=self.rsi_divergence_period)
indicator_kc = KeltnerChannel(high=df[«HIGH»],low=df[«LOW»],close=df[«CLOSE»], window=50, window_atr=50,fillna=False,multiplier=5, original_version=False)
df[«KC_UPPER»] = indicator_kc.keltner_channel_hband()
df[«KC_MIDDLE»] = indicator_kc.keltner_channel_mband()
df[«KC_LOWER»] = indicator_kc.keltner_channel_lband()
return df
except Exception as e:
print(‘error while calculating technicals’)
print(f’exception occured {e}’)

TA = TechAnalysis()

Попробуйте построить график с индикаторами в excel и обратите внимание, насколько он похож по сравнению с инструментами построения графиков. Не стесняйтесь обращаться к документации обеих библиотек и расширять и добавлять столько технических индикаторов, сколько необходимо.

В следующем посте мы подробно рассмотрим, как разрабатывать торговые стратегии на основе индикаторов, а также как тестировать каждую стратегию, чтобы понять, какая из них более эффективна. Далее мы увидим, как автоматизировать торговлю с помощью Python на основе выбранной стратегии и разработать торгового бота, который автоматически выполняет покупки, продажи в платформе Zerodha Kite. В последних нескольких постах мы также увидим, как использовать LLM для сбора новостей из нескольких источников и прогнозирования влияния, которое они могут оказать на рынок.

Часть II. Стратегии тестирования на истории

Этот пост является частью нашей серии статей об использовании Python и LLM для объединения технического анализа с рыночными новостями в режиме реального времени для точной настройки торговых решений на основе потенциального влияния новостей на рынок.

В прошлой статье мы рассмотрели основы получения исторических данных и добавления технических индикаторов, таких как EMA, RSI и стратегия Кельтнера. В этом посте мы углубимся в тестирование нескольких стратегий, основанных на этих индикаторах.

Почему важно тестировать на истории перед применением стратегии вживую?

Важность тестирования на истории перед реализацией стратегии в реальной торговле заключается в его способности оценить историческую эффективность стратегии. Несмотря на то, что история не всегда воспроизводится в точности, она часто дает ценные сведения. Ведь индикаторы служат сложным средством постижения коллективных эмоций трейдеров на рынке. Поэтому я настоятельно рекомендую выявлять закономерности в рамках конкретных секторов, отдельных акций и различных рыночных условий. Некоторые стратегии могут превзойти другие в определенных отраслях и при определенных рыночных условиях, и наиболее эффективным способом убедиться в этом является тщательное тестирование на истории.

Давайте создадим несколько простых стратегий и протестируем каждую стратегию на истории.

  1. Стратегия 1: покупаем на уровне RSI 20, продаем на уровне RSI 80
  2. Стратегия 2: Покупайте, когда текущая цена равна или ниже минимума Кельтнера, продавайте, когда текущая цена равна или выше максимума Кельтнера.
  3. Стратегия 3:
  • Золотой крест, когда 50 DMA пересекает 200 DMA снизу вверх, подтверждая бычий сигнал, Совершите покупку
  • Death Cross, когда 50 DMA пересекает DMA сверху, подтверждая медвежий сигнал, совершаем продажу

Заметка:

  1. Несмотря на то, что вышеперечисленные индикаторы хороши, они не являются надежными и, как и любые торговые стратегии, сопряжены с рисками. Могут возникать ложные сигналы, и стратегия может работать не во всех рыночных условиях. Эти сигналы в сочетании с другими сигналами могут дать лучшие результаты.
  2. Хорошие торговые решения требуют гораздо более комбинированной стратегии. Этот пост является лишь путем к разработке более сложных стратегий и не должен использоваться как таковой. Не применяйте их в сделках в режиме реального времени, если они не проверены должным образом

Стратегия 1 — Уровни RSI

В нашей предыдущей статье мы рассмотрели, как рассчитывать различные технические индикаторы на основе исторических данных и сохранять их в CSV. В этом сегменте мы опишем шаги по внедрению фреймворка для тестирования на истории, закладывая основу для торговой стратегии. Эти шаги включают в себя:

  • Чтение CSV-файла и его сохранение в Pandas DataFrame
  • Итерация по строкам DataFrame
  • Выполнение фиктивной покупки (когда последуют стратегии покупки и продажи)
  • Выполнение фиктивных продаж и отслеживание прибылей и убытков
  • Создание комплексного отчета по всем покупкам и продажам и хранение его в CSV

Запустим класс Strategy и метод buy для выполнения простой фиктивной покупки в прошлом. (Когда покупать, а когда продавать, чуть позже).

strategy.pyclass Strategy:
def __init__(self):
self.default_quantity = 100
self.buy_limit = 100000

def buy_stock(self,row,current_state,order_df,default_quantity,tick):
if current_state[‘quantity’]==0:
current_state[‘start_date’] = row[‘TIMESTAMP’]
row_df = pd.DataFrame({‘tick’:[tick],’TIMESTAMP’: [row[‘TIMESTAMP’]],’action’:[‘BUY’],’price’:[row[‘CLOSE’]],’quantity’:[default_quantity],’pnl’:[0],’percent’:[0],
‘days_held’:[0]})
order_df = pd.concat([order_df,row_df],ignore_index=True)
new_q = current_state[‘quantity’] + default_quantity
current_state[‘avg_price’] = ((current_state[‘quantity’]*current_state[‘avg_price’]) + (default_quantity*row[‘CLOSE’]))/new_q
current_state[‘quantity’] = new_q
current_investment = default_quantity*row[‘CLOSE’]
return order_df,current_state

Strategy мы выполняем действия на покупку и поддерживаем важные метрики, такие как текущее количество и средняя цена, которые изначально равны 0. Во время покупки мы обновляем эти метрики. Несмотря на то, что детали расчета средней цены могут быть довольно стандартными и включать в себя математические вычисления, это сердце нашей стратегии.

Теперь давайте углубимся в процесс продажи и проведем сравнение с действиями на покупку.def sell_stock(self,row,current_state,order_df,tick,total_pnl):
current_pnl = (current_state[‘quantity’]*(row[‘CLOSE’]-current_state[‘avg_price’]))
percentage = (current_pnl/(current_state[‘quantity’] * current_state[‘avg_price’]))*100
days_held = (row[‘TIMESTAMP’] — current_state[‘start_date’]).days
row_df = pd.DataFrame({‘tick’:[tick],’TIMESTAMP’: [row[‘TIMESTAMP’]],’action’:[‘SELL’],’price’:[row[‘CLOSE’]],’quantity’:[current_state[‘quantity’]],’pnl’:[current_pnl], ‘percent’:[percentage],
‘days_held’:[days_held]})
order_df = pd.concat([order_df,row_df],ignore_index=True)
new_fund = (current_state[‘quantity’]*row[‘CLOSE’])
percentage = (current_pnl/(current_state[‘quantity’] * current_state[‘avg_price’]))*100
total_pnl+=current_pnl
current_state = {‘quantity’:0, ‘avg_price’:0}
return order_df, current_state,total_pnl

Посмотрите, как мы поддерживаем order_df, который служит рекордом как для сделок на покупку, так и для продажи. Эта запись будет играть решающую роль в создании последующих подробных отчетов.

Во время продажи мы выполняем несколько дополнительных задач. Мы рассчитываем прибыль и убыток (P&L) по текущей сделке, а также совокупный P&L по всем сделкам. Кроме того, мы определяем количество дней, в течение которых акции удерживались, и рассчитываем процент P&L для каждой завершенной сделки. Эти подробные данные играют важную роль в оценке эффективности каждой торговой стратегии.

Далее мы рассмотрим конкретные критерии для принятия решения о том, когда покупать, а когда продавать, с отдельными классами, посвященными каждой стратегии.class RSIStrategy(Strategy):
def __init__(self):
self.default_quantity = 100
self.buy_limit = 100000

def execute(self, row, order_df, current_state):
tick = row[‘SYMBOL’]
total_pnl = 0
already_bought = current_state[‘quantity’] * current_state[‘avg_price’]
if row[‘RSI14’]>=80:
if current_state[‘quantity’]>0 and row[‘CLOSE’] > current_state[‘avg_price’]:
order_df, current_state,total_pnl = super().sell_stock(row,current_state,order_df,tick,total_pnl)

elif row[‘RSI14’]<=20:
order_df,current_state = super().buy_stock(row,current_state,order_df,self.default_quantity,tick)
return order_df,current_state

Класс RSIStrategy является подклассом Strategy. Это наследование позволяет нам повторно использовать методы ‘buy’ и ‘sell’ из родительского класса во всех подклассах, которые мы создаем.

Обратите внимание, что значения ’20’ и ’80’ в настоящее время жестко запрограммированы в стратегии. В следующей статье мы параметризуем эти значения, что позволит нам регулировать их динамически, возможно, с помощью пользовательского интерфейса. Наш следующий шаг — внедрить фреймворк для тестирования на истории в пользовательский интерфейс Streamlit, что позволит нам эффективно визуализировать результаты.

Кроме того, поскольку мы планируем переключаться между несколькими торговыми стратегиями, мы создадим фабричный класс. Этот класс будет возвращать экземпляры на основе определенных параметров, таких как имя стратегии.class StrategyFactory:
@staticmethod
def create_strategy(strategy):
if strategy==»Keltner»:
return KeltnerStrategy()
elif strategy==»MA»:
return MovingAverageStrategy()
elif strategy==»RSI»:
return RSIStrategy()
else:
raise ValueError(«invalid.»)

Примечание: сейчас мы разработали только RSIStrategy(). Остальные 2 стратегии мы рассмотрим ближе к концу. Теперь, когда мы завершили нашу важную часть, следующим шагом станет то, как мы будем использовать Стратегию.

Расход стратегии

Давайте создадим новый файл backtest.py, который будет использовать стратегию, которую мы только что создали

backtest.pyimport pandas as pd
import os
from csvwriter import write_to_excel
from strategy import StrategyFactory
class BackTest:
def __init__(self):
self.folder = ‘./historical/analysis/’
self.files = os.listdir(self.folder)
self.sheets = [«day»]
self.indicator = «RSI»
self.strategy = StrategyFactory.create_strategy(self.indicator)
self.output_file_path = f’./back_test/{self.indicator}_back_tested.csv’

В нашем фреймворке self.folder служит местом хранения исторических данных в формате CSV. Мы используем стратегию RSI, и результирующий CSV-файл, содержащий запись о действиях на покупку и продажу, хранится в self.output_file_path.

Теперь давайте перейдем к методу back_test, который выполняет шаги, которые мы обсудили. Вот что происходит:

  1. Начнем с чтения CSV-файла с историческими данными.
  2. Инициализируем current_state.
  3. Затем мы перебираем строки и вызываем self.strategy.execute, где в игру вступает фактический метод execute каждого объекта Strategy.
  4. После выполнения этих действий мы рассчитываем нереализованную прибыль и убыток (P&L). Стоит отметить, что бывают ситуации, когда происходит покупка, но соответствующей продажи не следует. В таких случаях мы рассчитываем и отслеживаем активы.

def back_test_a_stock(self, filename):
tick = filename
unrealized=0
unrealized_percent = 0
df = pd.read_excel(self.folder+filename, sheet_name=self.sheet)

df[‘TIMESTAMP’] = pd.to_datetime(df[‘TIMESTAMP’])
current_state = {‘quantity’:0, ‘avg_price’:0, ‘start_date’:None}

order_df = pd.DataFrame()
for index, row in df.iloc[21:].iterrows():
order_df,current_state = self.strategy.execute(row,order_df,current_state)

if current_state[‘quantity’]>0:
q = current_state[‘quantity’]
a = current_state[‘avg_price’]
last_price = df.iloc[-1][«CLOSE»]
unrealized = (last_price-a)*q
unrealized_percent = (last_price-a)/a*100
return order_df,unrealized, unrealized_percent

Мы успешно реализовали процесс тестирования на истории для одного (стокового) CSV-файла. Однако реальные сценарии часто включают в себя несколько акций, каждая из которых имеет свои исторические данные. Чтобы решить эту проблему, мы разработаем еще один метод в том же классе. def strategy_test(self):
#part1: Iterate through all the files
back_tested_result = pd.DataFrame()
unrealized_total=0
unrealized_percent = []
for filename in self.files:
if filename.endswith(‘.xlsx’) and not filename.startswith(‘~$’):
result,unrealized,u_percent = self.back_test_a_stock(filename)
if u_percent!=0:
unrealized_percent.append(u_percent)

unrealized_total+=unrealized
back_tested_result = pd.concat([back_tested_result, result], ignore_index=True)
#part 2: calculate profit percentage and total days held
back_tested_result.to_csv(self.output_file_path, index=False)
avg_profit_percentage = back_tested_result[back_tested_result[‘percent’] != 0][‘percent’].mean()
avg_days_held = back_tested_result[back_tested_result[‘days_held’] != 0][‘days_held’].mean()
realized_total = back_tested_result[‘pnl’].sum()
#part3: print the results
print(f’Average realized pnl % is {avg_profit_percentage}%’)
print(f’Average holding days is {avg_days_held}’)
if len(unrealized_percent) > 0:
avg_profit_percentage_u = sum(unrealized_percent)/len(unrealized_percent)
print(f’Total unrealized % is {avg_profit_percentage_u}%’)

Хотя код может показаться обширным и раздражающим (так оно и было, когда я читал!), по сути он состоит из трех фундаментальных операций:

  1. Инициализация ключевых параметров.
  2. Итерация по каждому файлу исторических данных с применением метода тестирования на истории.
  3. Расчет ключевых метрик для оценки эффективности стратегии, в том числе:
  • Средняя реализованная прибыль и убыток (P&L).
  • Общая нереализованная прибыль и убытки (еще не реализованы).
  • Количество дней удержания.

Эти метрики служат ценными индикаторами для оценки успешности или неудачи краткосрочной торговой стратегии. Стоит отметить, что, хотя это важно, другие факторы также могут быть рассмотрены в зависимости от конкретных торговых целей.

Наконец, инициализируем наш класс BackTestbt = BackTest()
bt.strategy_test()

Это создаст CSV-файл со списком покупок и продаж, упорядоченных по датам, а также напечатает прибыль и убыток, как реализованные, так и нереализованные, а также среднее количество дней удержания.

На сегодняшний день мы разработали

  • backtest.py
  • strategy.py
  • tech_analysis.py (предыдущая запись)
  • tech_data.py(предыдущая запись)

Убедитесь, что вы создали 2 папки: /historical/analyis (здесь хранятся все исторические данные CSV) и /back_test (где хранится каждый экземпляр результата тестирования). Это полная настройка, необходимая для полного запуска одной стратегии. Давайте создадим еще 2 стратегии.

Стратегия 2 — Каналы Кельтнера

Отличное введение об этой стратегии здесь.class KeltnerStrategy(Strategy):
def __init__(self):
self.default_quantity = 100
self.buy_limit = 100000

def execute(self, row, order_df, current_state):
tick = row[‘SYMBOL’]
total_pnl = 0
already_bought = current_state[‘quantity’] * current_state[‘avg_price’]
buy_support_level = current_state[‘avg_price’]-(current_state[‘avg_price’]/100)
good_buy_range = (row[‘KC_LOWER’]+(row[‘KC_LOWER’]/400))

if row[‘CLOSE’]>=row[‘KC_UPPER’]:
if current_state[‘quantity’]>0 and row[‘CLOSE’] > current_state[‘avg_price’]:
order_df, current_state,total_pnl = super().sell_stock(row,current_state,order_df,tick,total_pnl)


elif row[‘CLOSE’]<=row[‘KC_LOWER’]:
eq = 2*self.default_quantity
order_df,current_state = super().buy_stock(row,current_state,order_df,eq,tick)

elif row[‘CLOSE’]<=buy_support_level:
order_df,current_state = super().buy_stock(row,current_state,order_df,self.default_quantity,tick)

elif (current_state[‘quantity’]==0 and row[‘CLOSE’]<=good_buy_range):
order_df,current_state = super().buy_stock(row,current_state,order_df,self.default_quantity//2,tick)
else:
pass
return order_df,current_state

Здесь мы добавляем немного больше сложности в нашу стратегию.

  • Покупайте, когда цена ближе к KC Lower
  • Покупайте 2X, когда цена ниже KC Lower
  • Средняя, если цена ниже 1% от нашего среднего уровня владения
  • Продавать, когда цена выше KC Upper

Как мы убедились, возможности для создания и тестирования торговых стратегий огромны. Вы можете ввести столько условий, сколько посчитаете нужным, и даже комбинировать несколько индикаторов для более полного анализа. На самом деле, использование нескольких индикаторов для подтверждения доказало свою эффективность.

На этом этапе я призываю наших читателей отправиться в увлекательное путешествие по созданию своих собственных торговых стратегий, независимо от того, являются ли они простыми или сложными. Экспериментируйте и тестируйте эти стратегии самостоятельно. Поэтому я намеренно пропускаю реализацию стратегии EMA. Он будет на github для справки.

В третьей части нашей серии статей мы рассмотрим использование Streamlit для визуального тестирования этих стратегий и создания высококачественных биржевых графиков с результатами. В конечном счете, мы углубимся в один из самых важных аспектов этой серии: тонкую настройку языковой модели (LLM) с помощью рыночных данных. Это позволит нам использовать LLM наряду с нашими техническими стратегиями для принятия более обоснованных и основанных на данных торговых решений.

Часть III — Торговая система и ежедневные новости

Если вы дневной трейдер, свинг-трейдер или активно следите за деловыми новостями, вы, вероятно, знаете о значительном влиянии, которое новости могут оказать на рынки — иногда это волнующе, а иногда расстраивает, если мы упускаем подходящий момент для действий.

В то время как фундаментальный анализ обеспечивает прочную основу для принятия стратегических решений, сочетание его с техническим анализом улучшает наш подход. Сделав еще один шаг вперед, интегрируя текущие рыночные условия, особенно новости, можно получить более полную картину. В этой статье исследуется интеграция этих факторов в наши торговые решения.

Для тех из нас, кто занимается торговлей неполный рабочий день, ручной анализ нескольких источников деловых новостей, оценка настроений на рынке и принятие повседневных решений может быть сложной задачей. Несмотря на то, что такой ручной подход дает ценный опыт обучения, существует потребность в масштабируемости. Именно здесь мы обращаемся к возможностям Python, моделям LLM и науке о данных, чтобы оптимизировать процесс. В этой статье мы углубимся в это увлекательное пересечение.

  1. Извлечение списка последних новостных статей из API агрегатора новостей.
  2. Свяжитесь с каждым новостным источником и соберите полные статьи.
  3. Резюмируйте статью с помощью LLM.
  4. Анализ настроений с использованием моделей Finance LLM (Bloomberg, PaLM Finance model)
  5. Объедините их с Техническими индикаторами и постройте рекомендательную систему.

Обо всем по порядку!

Являются ли языковые модели (LLM) настоящими финансовыми экспертами, если им предоставлены правильные данные? Продолжающиеся исследования в этой области говорят о том, что их пока нет. Несмотря на то, что есть несколько многообещающих моделей, их надежность не является абсолютной. Заслуживающая внимания исследовательская статья, проливающая свет на этот вопрос, доступна на сайте https://arxiv.org/pdf/2310.12664.pdf.

Теперь возникает вопрос: почему мы должны рассмотреть возможность использования LLM для финансов? На мой взгляд, потенциал кроется в будущем. Со временем мы ожидаем разработки очень сложных моделей, которые могут предложить экспертную информацию в области финансов. В рамках этой серии статей мы подробнее рассмотрим эту концепцию. В заключение мы приступим к тонкой настройке нашей собственной модели Finance LLM, используя набор данных, состоящий из 50 000 статей о бизнесе. Этот практический подход продемонстрирует, как тонко настроенная модель может превзойти универсальные модели.

В сфере финансовых рынков прогнозирование реакций может быть исключительно сложной задачей. Например, объявления о положительных результатах могут привести к получению прибыли и последующему падению цен. И наоборот, отрицательный результат, если он менее отрицательный, чем ожидалось, может вызвать скачок цены. Сценарии разнообразны и сложны. Только модели, специально обученные для таких сложных случаев, могут принимать более точные решения в таких ситуациях.

Так что же мы строим?

Взгляните на них ниже. Окончательная структура файла выглядит следующим образом.

API новостного агрегатора

Во-первых, нам нужен способ собрать все ежедневные новости в одном месте. Вот тут-то и пригодится newsapi.org. Нас особенно интересуют их деловые новости. К счастью, newsapi имеет как REST API, так и библиотеку python, которую мы можем установить и использовать. Давайте установим эту библиотеку с помощьюpip3 install newsapi-python

Зарегистрируйте свою учетную запись и получите их API Key. Несмотря на то, что сервис стоит за использование в бизнесе, его версия для разработчиков имеет ограниченные возможности и бесплатна для использования.

Давайте создадим простой python-класс, который извлекает новостные статьи.#news_api.py
class NewsApi:
def __init__(self):
self.token = os.getenv(«NEWS_API_TOKEN»)
self.country = ‘in’
self.category=’business’
self.csv_location = ‘./news/’
self.filename = f’news_{date.today()}.csv’
self.newsapi = NewsApiClient(api_key=self.token) def extract_news(self):
page = 1
total_news_count = 0
all_articles = []
try:
while True:
top_headlines = self.newsapi.get_top_headlines(
country=self.country,
language=’en’,
category=self.category,
page=page
)
if top_headlines.get(‘status’)==’ok’:
for row in top_headlines.get(«articles»,None):
article = [row[‘publishedAt’],row[‘title’],row[‘description’],row[‘url’]]
all_articles.append(article)
page+=1

if len(all_articles)>=top_headlines[‘totalResults’]:
break
self.all_articles = pd.DataFrame(all_articles,columns=[‘Date’,’Title’,’Description’,’URL’])
self.all_articles.to_csv(f'{self.csv_location}{self.filename}’)
except Exception as e:
print(e)

Короче говоря, мы начинаем со страницы 1, извлекаем все новости на странице и переходим на следующую страницу. Мы собираем новости и храним их в all_articles переменной list. Наконец, мы сохраняем его в csv-файле. В то время как результаты API имеют несколько других столбцов, нас интересуют только Дата, Заголовок, Описание и URL.

Извлечение полных новостных статей

При изучении newsapi.org можно заметить, что новостные статьи усекаются сверх определенного ограничения по количеству символов. Однако спасением является то, что ссылка на первоисточник предоставлена. В связи с этим возникает острая необходимость в извлечении полного новостного контента. Таким образом, мы даем возможность специалистам по большим языковым моделям (LLM) читать новости целиком, что позволяет им делать более обоснованные суждения о тональности новостей.

В нашем стремлении использовать весь потенциал LLM извлечение полных новостных статей становится ключевым шагом. Это не только обеспечивает всестороннее понимание содержания, но и позволяет более точно анализировать настроения, что в конечном итоге повышает эффективность наших торговых решений.

Можно ли копировать новостные статьи с сайта?

Ответ заключается в том, что это зависит от условий и положений сайта. Но большинство новостных статей могут быть удалены, поскольку вся информация намеренно становится доступной для общественности. Все общедоступные данные могут быть извлечены, если ваш метод не наносит вреда данным или владельцу веб-сайта.

Уже есть действительно хорошие статьи на средних каналах, написанные об утилизации новостных сайтов. Ниже я приведу ссылки.

  1. https://dorianlazar.medium.com/scraping-medium-with-python-beautiful-soup-3314f898bbf5
  2. https://realpython.com/beautiful-soup-web-scraper-python/
  3. https://medium.com/@poojan.s/stock-market-news-scraper-using-beautifulsoup-bc7db5c75f99

Обратите внимание, что каждый новостной сайт уникален, и нам нужно будет по-разному скрейсить для разных источников. Это классический вариант использования FactoryPattern.

Несмотря на то, что я пропустил написание о парсинге новостей, вот мой завершенный код для парсинга из разных новостных статей. Следите за моим github, так как я активно продолжаю добавлять больше новостей при каждом обновлении.

Суммирование и анализ тональности с LLM

В то время как я лично использую PaLM2 для обобщения и BloombergGPT от Huggingface Hub, который я настроил с помощью приличного набора данных бизнес-новостей для получения новостных настроений, в этой статье я буду использовать PaLM2 от Google для обеих целей. В связи с недавними разговорами о Gemini, самом известном мультимодальном самолете, созданном Google, PaLM2 определенно стоит попробовать!

Примечание: Данная статья предназначена для того, чтобы показать техническую возможность использования LLM для помощи в принятии торговых решений. Читателям предлагается провести тщательное тестирование и проявить должную осмотрительность, прежде чем использовать его для принятия решений в режиме реального времени.

Давайте начнем с создания простого интерфейса PaLM2, который принимает пользовательский запрос и извлекает результаты из LLM через Rest API. Далее мы создадим простую подсказку, которая поможет нам извлечь данные нужным нам способом.

  • Для начала нам понадобится ключ API. Перейдите по этой ссылке и получите его. https://developers.generativeai.google/products/palm
  • Убедитесь, что вы сохранили ключ API в файле .env как PALM2_API_TOKEN

Установить клиент palm2 довольно простоpip3 install google-generativeai#palm_interface.py
class PalmInterface:
def prompt(self,query):
try:
payload = self.build_input(query)
defaults = { ‘model’: ‘models/text-bison-001’ }
response = palm.generate_text(**defaults, prompt=payload)
json_response = json.loads(response.result)
return json_response
except Exception as e:
print(e)
return None

Пришло время для быстрого проектирования…def summarize(self,full_news):
template3 = f»You excel in succinctly summarizing business and finance-related news articles.\
Upon receiving a news article, your objective is to craft a concise and accurate summary while \
retaining the name of the company mentioned in the original article. The essence of the article \
should be preserved in your summary. A job well done in summarizing may earn you a generous tip.\
Please proceed with the provided full news article. {full_news}»
try:
defaults = { ‘model’: ‘models/text-bison-001’ }
response = palm.generate_text(**defaults, prompt=template3)
return response.result
except Exception as e:
print(e)
return None

Оперативное проектирование… Если этот термин звучит для вас впервые, думайте о Побуждении как о Йоде проявления. Чем яснее ваши просьбы к Вселенной в сочетании с непоколебимой верой, тем больше вероятность того, что ваши желания окажутся у вас на коленях. 🙂 Но дело не только в этом — давайте сделаем передышку, переварим эту пижаму, а затем быстро забудем о ней.

На этот раз несколько выстрелов…def build_input(self,article):
article_1 = «Tech giant Apple Inc. reports record-breaking quarterly earnings, surpassing market expectations and driving stock prices to new highs. Investors express optimism for the company’s future prospects.»
article_2 = «Alphabet Inc., the parent company of Google, faces a setback as regulatory concerns lead to a sharp decline in share prices. The market reacts negatively to uncertainties surrounding the company’s antitrust issues.»
prompt = f»»»
Few-shot prompt:
Task: Analyze the impact of the news on stock prices.
Instructions: As a seasoned finance expert specializing in the Indian stock market, you possess a keen understanding \
of how news articles can influence market dynamics. In this task, you will be provided with a news article \
or analysis. Upon thoroughly reading the article, if it contains specific information about a company’s \
stock, please provide the associated Stock Symbol (NSE or BSE Symbol), the Name of the stock, and the \
anticipated Impact of the news.The Impact value should range between -1.0 and 1.0, with -1.0 signifying \
highly negative news likely to cause a significant decline in the stock price in the coming days/weeks, \
and +1.0 representing highly positive news likely to lead to a surge in share price in the next few days/weeks.\
Your response must be strictly in the JSON format.Consider the following factors while determining the impact: \
The magnitude of the news, The sentiment of the news,Market conditions at the date of the news, Liquidity \
of the stock, The sector in which the company operates, The JSON response should include the keys: symbol, \
name, and impact. Do not consider indices such as NIFTY. If the news is not related to the stock market or any \
specific company, leave the values blank. Do not invent values; maintain accuracy and integrity in your response.\
Examples:
1. Article: «{article_1}»
Response: {{«symbol»: «AAPL», «name»: «Apple Inc.», «impact»: 0.9}}

2. Article: «{article_2}»
Response: {{«symbol»: «GOOGL», «name»: «Alphabet Inc.», «impact»: -0.5}}

3. Article: «{article}»
Response:
«»»
return prompt

Что ж, определенно существуют лучшие методы подсказок, чем та, что описана выше.

Полезно почитать о методах подсказок — ознакомьтесь с ними и измените их по мере необходимости.

https://developers.generativeai.google/guide/prompt_best_practices

Несколько примеров, на которые можно вдохновиться https://developers.generativeai.google/prompt-gallery

Разработать торговую стратегию

До сих пор мы видели вещи по крупицам. Пришло время соединить их вместе.

Мы собираемся разработать очень простую стратегию, используя новостные настроения и RSI.

Strong Buy — Если новость имеет позитивный настрой и текущий RSI < 35 (зона перепроданности)

Покупать — если новость имеет позитивный настрой

Продавать — если новость имеет негативные настроения

Strong Sell — Если новость имеет негативные настроения и текущий RSI > 75 (зона перекупленности)

Стратегия довольно проста и не должна использоваться как есть, цель этого поста состоит в том, чтобы объединить новостные настроения и технические индикаторы, а не рекомендовать саму стратегию. Читателям предлагается разработать свою собственную проверенную стратегию и протестировать ее на истории. Иногда плохие новости о фундаментально сильных компаниях дают нам возможность купить. Негативные настроения иногда также могут стать возможностью для покупки.

Давайте воплотим эту стратегию в жизнь. Простой класс с 4 категориями.#news_tech_trader.py
from news_api import news
import pandas as pd
from palm_interface import palm_interface
from yahoo_finance import yfi
import os
from datetime import date
from news_scrapper import factory
class NewsTrader:
def __init__(self):
self.strong_buy = []
self.buy = []
self.strong_sell = []
self.sell = []

Импорт может вас немного смутить. Не переживай. Взгляните на GitHub, и все будет выглядеть нормально.

Вспомогательный метод для получения RSI для данного символа. def get_rsi(self, symbol):
row = yfi.download_data(symbol,’./data/’)
print(row)
if row is None:
return 50.0
else:
return row[‘RSI14’] if row[‘RSI14’] else 50.0

Поскольку мы уже обсуждали загрузку исторических данных, расчет RSI и т.д. в предыдущей части нашей серии, мы пропустим эту тему в этой статье. Обратитесь к github, чтобы узнать, как это делается. Также обратите внимание, что мы устанавливаем RSI по умолчанию равным 50, когда RSI недоступен. Мы устанавливаем для него нейронное значение, чтобы он не перепродавался и не перекуплен.

Теперь соединяем кусочки вместе…def run(self):

# PART — I — Check below explanation
results_list = []
extracted_news_file = f’./data/news/{date.today()}.csv’
df = pd.DataFrame()
if os.path.exists(extracted_news_file):
df = pd.read_csv(extracted_news_file)
else:
extracted_news_file = news.extract_news()
df = pd.read_csv(extracted_news_file)

# PART — II — Check below explanation
if ‘symbol’ not in df:
for index,row in df.iterrows():
text = factory.create_and_scrape(row[‘URL’])
if text is None or len(text)<10:
print(‘scrape not successful!’)
text = str(row[‘Title’]) + ‘ ‘ + str(row[‘Description’])
text = palm_interface.summarize(text)
data = palm_interface.prompt(text)
results_list.append({
‘symbol’: data[‘symbol’] if data else «»,
‘name’: data[‘name’] if data else «»,
‘impact’: data[‘impact’] if data else 0.0
})
df2 = pd.DataFrame(results_list)
df = pd.concat([df, df2], axis=1)
df.to_csv(extracted_news_file, index=False)

# PART — III, IV — Check below explanation
for index, row in df.iterrows():
if not pd.isna(row[‘symbol’]) and len(row[‘symbol’])>0:
rsi = self.get_rsi(row[‘symbol’])
if row[‘impact’] > 0.4:
if rsi <=35:
self.strong_buy.append(row[‘symbol’])
else:
self.buy.append(row[‘symbol’])
elif row[‘impact’] < -0.4:
if rsi >=75:
self.strong_sell.append(row[‘symbol’])
else:
self.sell.append(row[‘symbol’])

Не перегружайтесь длинными строками кода. На самом деле это проще, чем то, как это выглядит. Вот что происходит..

  1. Прочтите CSV-файл новости, если он существует. Если нет, сначала загрузите новостные статьи в формате CSV.
  2. Выполните итерацию по каждой статье, очистите всю статью по мере необходимости и сделайте вывод API Palm. Свяжите результаты вывода с самим новостным DF. Сохраните его обратно в тот же файл
  3. Повторите каждую статью еще раз. получить значение RSI для каждой акции, вызвав get_rsi.
  4. В зависимости от категории, в которую он попадает, храните в списке. Для некоторых статей Символ может быть недоступен или неверен. Пока все в порядке, в одной из следующих статей мы рассмотрим, как избавиться от таких проблем с помощью ElasticSearch и убедиться, что результаты извлечения из LLM правильные.

Полный код доступен на Github. Пожалуйста, пометьте проект звездочкой или ответвлением, если хотите.

Заключение

Одна из моих любимых цитат звучит так:

Большие инвестиционные возможности появляются, когда отличные компании окружены необычными обстоятельствами, которые приводят к неправильной оценке акций.

Уоррен Баффет

А что может быть лучше для понимания текущих обстоятельств компании, чем ее последние новости?

Реализация нашей стратегии здесь освежающе проста, но настоящее волнение возникает, когда мы смешиваем силу новостей и технического анализа с фундаментальными идеями. В следующих статьях мы будем использовать Screener API, чтобы добавить уровень сложности в режиме реального времени. Например, подумайте о долгосрочном влиянии новостей, таких как положительная выручка или увеличение книги заказов — оно часто выходит за рамки одного дня на рынке. И наоборот, негативные новости могут влиять на динамику акций в течение нескольких недель. Так зачем же ограничивать себя только одним днем новостей? В увлекательном упражнении для наших читателей мы призываем изучить новости за несколько дней для каждой компании, усреднить настроения, объединить объем и поэкспериментировать, чтобы найти золотую середину. Поверьте, это увлекательное путешествие.

В следующих статьях мы углубимся в тонкую настройку языковых моделей (LLM) с историческими новостными данными, сопоставив их с фактическими движениями цен и рассмотрев макроэкономические условия. Мы рассмотрим процесс публикации и тестирования модели, чтобы оценить ее производительность. Кроме того, мы займемся парсингом лент Twitter для зарегистрированных на бирже компаний, интегрируя важные фундаментальные данные, такие как соотношение долга к собственному капиталу (D/E), рентабельность собственного капитала (ROE), рентабельность задействованного капитала (ROCE), отношение цены к прибыли (P/E) и многое другое. Чтобы еще больше усовершенствовать рекомендательную систему, мы включим анализ движения объемов. Приготовьтесь к увлекательному путешествию, поскольку вместе мы создадим более ценную и всеобъемлющую систему рекомендаций.

Создание LLM с использованием Python

Изображение взято из GoogleDeepMind (открытый исходный код доступен на pexels)

Создание собственной большой языковой модели (LLM) — это крутая вещь, которую делают многие крупные компании, такие как Google, Twitter и Facebook. Они выпускают разные версии этих моделей, например, 7 миллиардов, 13 миллиардов или 70 миллиардов. Это делают и небольшие общины. Возможно, вы читали блоги или смотрели видео о создании собственного LLM, но в них обычно много говорится о теории, а не о реальных шагах и коде.

В этом блоге я попытаюсь сделать LLM всего с 2,3 миллионами параметров, и самое интересное, что для этого нам не понадобится модный графический процессор. Мы будем руководствоваться подходом LLaMA 1 Paper. Не переживай; Мы не будем усложнять задачу и использовать базовый набор данных, чтобы вы могли увидеть, как легко создать свой собственный LLM с миллионом параметров.

Необходимые условия

Убедитесь, что у вас есть базовое представление об объектно-ориентированном программировании (ООП) и нейронных сетях (NN). Знакомство с PyTorch также будет полезно при написании кода.

https://levelup.gitconnected.com/media/3b70dcf8fb754e844c066096fbf5709e

Общие сведения об архитектуре трансформатора LLaMA

Прежде чем углубляться в создание собственного LLM с использованием подхода LLaMA, важно понять архитектуру LLaMA. Ниже приведена сравнительная диаграмма между ванильным трансформатором и LLaMA.

Разница между трансформерами и архитектурой лам (архитектура лам Умара Джамиля)

Если вы не знакомы с архитектурой ванильного трансформатора, вы можете прочитать этот блог для получения базового руководства.

Давайте рассмотрим основные понятия LLaMA более подробно:

Предварительная нормализация с помощью RMSNorm:

В подходе LLaMA для нормализации входного сигнала каждого подслоя трансформатора используется метод RMSNorm. Этот метод основан на GPT-3 и предназначен для оптимизации вычислительных затрат, связанных с нормализацией слоев. RMSNorm обеспечивает производительность, аналогичную LayerNorm, но значительно сокращает время работы (на 7%∼64%).

Бумага для нормализации среднеквадратичного слоя (https://arxiv.org/abs/1910.07467)

Это достигается за счет упора на изменение масштаба инвариантности и регулирования суммированных входных данных на основе среднеквадратичной статистики (RMS). Основная мотивация состоит в том, чтобы упростить LayerNorm, удалив среднюю статистику. Заинтересованные читатели могут ознакомиться с подробной реализацией RMSNorm здесь.

Функция активации SwiGLU:

LLaMA представляет функцию активации SwiGLU, вдохновленную PaLM. Чтобы понять SwiGLU, необходимо сначала разобраться с функцией активации Swish. SwiGLU расширяет возможности Swish и включает в себя пользовательский уровень с плотной сетью для разделения и умножения входных активаций.

SwiGLU: Варианты GLU улучшают трансформатор (https://kikaben.com/swiglu-2020/)

Цель состоит в том, чтобы усилить выразительную силу модели за счет введения более сложной функции активации. Более подробную информацию о SwiGLU можно найти в соответствующем документе.

Поворотные закладные (RoPE):

Поворотные встраивания, или RoPE, — это тип позиционированного встраивания, используемый в LLaMA. Он кодирует абсолютную позиционную информацию с помощью матрицы вращения и, естественно, включает явную относительную зависимость положения в формулировки самовнимания. RoPE предлагает такие преимущества, как масштабируемость до различных длин последовательностей и уменьшение зависимости между токенами с увеличением относительных расстояний.

Это достигается путем кодирования относительных положений путем умножения на матрицу вращения, что приводит к затуханию относительных расстояний — желательная функция для кодирования на естественном языке. Те, кто интересуется математическими подробностями, могут обратиться к статье RoPE.

В дополнение к этим концепциям, в документе LLaMA представлены другие важные подходы, в том числе использование оптимизатора AdamW с определенными параметрами, эффективные реализации, такие как оператор каузального многоголового внимания, доступный в библиотеке xformers, и ручная реализация обратных функций для слоев трансформаторов для оптимизации вычислений во время обратных проходов.

Особая признательность и благодарность Ануш Кумар за подробное объяснение каждого важнейшего аспекта LLaMA.

Подготовка почвы

В этом проекте мы будем работать с рядом библиотек Python, поэтому давайте импортируем их:# PyTorch for implementing LLM (No GPU)
import torch

# Neural network modules and functions from PyTorch
from torch import nn
from torch.nn import functional as F

# NumPy for numerical operations
import numpy as np

# Matplotlib for plotting Loss etc.
from matplotlib import pyplot as plt

# Time module for tracking execution time
import time

# Pandas for data manipulation and analysis
import pandas as pd

# urllib for handling URL requests (Downloading Dataset)
import urllib.request

Кроме того, я создаю конфигурационный объект, в котором хранятся параметры модели.# Configuration object for model parameters
MASTER_CONFIG = {
# Adding parameters later
}

Такой подход обеспечивает гибкость, позволяя добавлять дополнительные параметры по мере необходимости в будущем.

Предварительная обработка данных

В оригинальной статье LLaMA для обучения и оценки модели использовались различные наборы данных из открытых источников.

К сожалению, использование обширных наборов данных может быть непрактичным для небольших проектов. Поэтому для нашей реализации мы будем использовать более скромный подход, создав значительно уменьшенную версию LLaMA.

Учитывая ограничения, связанные с отсутствием доступа к огромным объемам данных, мы сосредоточимся на обучении упрощенной версии LLaMA с использованием набора данных TinyShakespeare. Этот набор данных из открытых источников, доступный здесь, содержит около 40 000 строк текста из различных произведений Шекспира. На этот выбор повлияла серия Makemore от Karpathy, которая дает ценную информацию об обучении языковых моделей.

В то время как LLaMA был обучен на обширном наборе данных, содержащем 1,4 триллиона токенов, наш набор данных, TinyShakespeare, содержит около 1 миллиона символов.

Во-первых, давайте получим наш набор данных, загрузив его:# The URL of the raw text file on GitHub
url = «https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt»

# The file name for local storage
file_name = «tinyshakespeare.txt»

# Execute the download
urllib.request.urlretrieve(url, file_name)

Этот скрипт Python извлекает набор данных tinyshakespeare из указанного URL-адреса и сохраняет его локально с именем файла «tinyshakespeare.txt».

Теперь давайте определим размер словаря, который представляет собой уникальное количество символов в нашем наборе данных. Вот фрагмент кода:# Read the content of the dataset
lines = open(«tinyshakespeare.txt», ‘r’).read()

# Create a sorted list of unique characters in the dataset
vocab = sorted(list(set(lines)))

# Display the first 10 characters in the vocabulary list
print(‘Printing the first 10 characters of the vocab list:’, vocab[:10])

# Output the total number of characters in our dataset (Vocabulary Size)
print(‘Total number of characters in our dataset (Vocabulary Size):’, len(vocab))

Теперь мы создаем сопоставления между целыми числами и символами (itos) и символами с целыми числами (stoi). Вот код:# Mapping integers to characters (itos)
itos = {i: ch for i, ch in enumerate(vocab)}

# Mapping characters to integers (stoi)
stoi = {ch: i for i, ch in enumerate(vocab)}

В оригинальной статье LLaMA использовался маркеризатор кодирования байт-пар SentencePiece от Google. Однако для простоты мы выберем базовый токенизатор на уровне символов. Давайте создадим функции кодирования и декодирования, которые позже применим к нашему набору данных:# Encode function: Converts a string to a list of integers using the mapping stoi
def encode(s):
return [stoi[ch] for ch in s]

# Decode function: Converts a list of integers back to a string using the mapping itos
def decode(l):
return ».join([itos[i] for i in l])

# Example: Encode the string «hello» and then decode the result
decode(encode(«morning»))

Последняя строка, которая будет выведена morning , подтверждает надлежащую функциональность функций кодирования и декодирования.

Теперь мы преобразуем наш набор данных в тензор резака, указывая его тип данных для дальнейших операций с помощью PyTorch:# Convert the dataset into a torch tensor with specified data type (dtype)
dataset = torch.tensor(encode(lines), dtype=torch.int8)

# Display the shape of the resulting tensor
print(dataset.shape)

На выходе получаетсяtorch.Size([1115394]) указывает, что наш набор данных содержит примерно один миллион маркеров. Стоит отметить, что это значительно меньше, чем датасет LLaMA, который состоит из 1,4 триллиона токенов.

Мы создадим функцию, отвечающую за разделение нашего набора данных на обучающие, проверочные или тестовые наборы. В проектах машинного обучения или глубокого обучения такие разделения имеют решающее значение для разработки и оценки моделей, и тот же принцип применим здесь при воспроизведении подхода Large Language Model (LLM):# Function to get batches for training, validation, or testing
def get_batches(data, split, batch_size, context_window, config=MASTER_CONFIG):
# Split the dataset into training, validation, and test sets
train = data[:int(.8 * len(data))]
val = data[int(.8 * len(data)): int(.9 * len(data))]
test = data[int(.9 * len(data)):]

# Determine which split to use
batch_data = train
if split == ‘val’:
batch_data = val
if split == ‘test’:
batch_data = test

# Pick random starting points within the data
ix = torch.randint(0, batch_data.size(0) — context_window — 1, (batch_size,))

# Create input sequences (x) and corresponding target sequences (y)
x = torch.stack([batch_data[i:i+context_window] for i in ix]).long()
y = torch.stack([batch_data[i+1:i+context_window+1] for i in ix]).long()

return x, y

Теперь, когда наша функция разбиения определена, давайте установим два параметра, решающих для этого процесса:# Update the MASTER_CONFIG with batch_size and context_window parameters
MASTER_CONFIG.update({
‘batch_size’: 8, # Number of batches to be processed at each random split
‘context_window’: 16 # Number of characters in each input (x) and target (y) sequence of each batch
})

batch_size определяет, сколько пакетов обрабатывается при каждом случайном разбиении, в то время как context_window определяет количество символов в каждой входной (x) и целевой (y) последовательности каждого пакета.

Давайте напечатаем случайную выборку из разделения поездов пакета 8 и контекстного окна 16 из нашего набора данных:# Obtain batches for training using the specified batch size and context window
xs, ys = get_batches(dataset, ‘train’, MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’])

# Decode the sequences to obtain the corresponding text representations
decoded_samples = [(decode(xs[i].tolist()), decode(ys[i].tolist())) for i in range(len(xs))]

# Print the random sample
print(decoded_samples)

Стратегия оценки

Теперь мы создадим функцию, предназначенную для оценки нашей самостоятельно созданной архитектуры LLaMA. Причина, по которой это делается перед определением фактического модельного подхода, заключается в том, чтобы обеспечить непрерывную оценку в процессе обучения.@torch.no_grad() # Don’t compute gradients for this function
def evaluate_loss(model, config=MASTER_CONFIG):
# Placeholder for the evaluation results
out = {}

# Set the model to evaluation mode
model.eval()

# Iterate through training and validation splits
for split in [«train», «val»]:
# Placeholder for individual losses
losses = []

# Generate 10 batches for evaluation
for _ in range(10):
# Get input sequences (xb) and target sequences (yb)
xb, yb = get_batches(dataset, split, config[‘batch_size’], config[‘context_window’])

# Perform model inference and calculate the loss
_, loss = model(xb, yb)

# Append the loss to the list
losses.append(loss.item())

# Calculate the mean loss for the split and store it in the output dictionary
out[split] = np.mean(losses)

# Set the model back to training mode
model.train()

return out

Мы использовали потери в качестве метрики для оценки производительности модели во время итераций обучения. Наша функция выполняет итерацию по обучающим и проверочным сплитам, вычисляет средние потери за 10 пакетов для каждого разбиения и, наконец, возвращает результаты. Затем модель возвращается в режим обучения с помощью model.train().

Настройка базовой модели нейронной сети

Мы строим базовую нейронную сеть, которую в дальнейшем будем совершенствовать с помощью методов LLaMA.# Definition of a basic neural network class
class SimpleBrokenModel(nn.Module):
def __init__(self, config=MASTER_CONFIG):
super().__init__()
self.config = config

# Embedding layer to convert character indices to vectors (vocab size: 65)
self.embedding = nn.Embedding(config[‘vocab_size’], config[‘d_model’])

# Linear layers for modeling relationships between features
# (to be updated with SwiGLU activation function as in LLaMA)
self.linear = nn.Sequential(
nn.Linear(config[‘d_model’], config[‘d_model’]),
nn.ReLU(), # Currently using ReLU, will be replaced with SwiGLU as in LLaMA
nn.Linear(config[‘d_model’], config[‘vocab_size’]),
)

# Print the total number of model parameters
print(«Model parameters:», sum([m.numel() for m in self.parameters()]))

В текущей архитектуре уровень встраивания имеет размер словаря 65, представляющий символы в нашем наборе данных. Поскольку это служит нашей базовой моделью, мы используем ReLU в качестве функции активации в линейных слоях; однако позже он будет заменен на SwiGLU, который используется в LLaMA.

Чтобы создать прямой проход для нашей базовой модели, мы должны определить прямую функцию в нашей модели NN.# Definition of a basic neural network class
class SimpleBrokenModel(nn.Module):
def __init__(self, config=MASTER_CONFIG):

# Rest of the code

# Forward pass function for the base model
def forward(self, idx, targets=None):
# Embedding layer converts character indices to vectors
x = self.embedding(idx)

# Linear layers for modeling relationships between features
a = self.linear(x)

# Apply softmax activation to obtain probability distribution
logits = F.softmax(a, dim=-1)

# If targets are provided, calculate and return the cross-entropy loss
if targets is not None:
# Reshape logits and targets for cross-entropy calculation
loss = F.cross_entropy(logits.view(-1, self.config[‘vocab_size’]), targets.view(-1))
return logits, loss

# If targets are not provided, return the logits
else:
return logits

# Print the total number of model parameters
print(«Model parameters:», sum([m.numel() for m in self.parameters()]))

Эта функция прямого прохода принимает символьные индексы (idx) в качестве входных данных, применяет уровень внедрения, пропускает результат через линейные слои, применяет активацию softmax для получения распределения вероятностей (logits). Если указаны целевые значения, он вычисляет потери перекрестной энтропии и возвращает как логиты, так и потери. Если целевые объекты не указаны, он возвращает только логиты.

Чтобы создать экземпляр этой модели, мы можем напрямую вызвать класс и вывести общее количество параметров в нашей модели простой нейронной сети. Мы установили размерность наших линейных слоев равной 128, указав это значение в нашем объекте конфигурации:# Update MASTER_CONFIG with the dimension of linear layers (128)
MASTER_CONFIG.update({
‘d_model’: 128,
})

# Instantiate the SimpleBrokenModel using the updated MASTER_CONFIG
model = SimpleBrokenModel(MASTER_CONFIG)

# Print the total number of parameters in the model
print(«Total number of parameters in the Simple Neural Network Model:», sum([m.numel() for m in model.parameters()]))

Наша простая модель нейронной сети содержит около 33 000 параметров.

Точно так же, чтобы вычислить логиты и потери, нам нужно только передать наш разделенный набор данных в нашу модель:# Obtain batches for training using the specified batch size and context window
xs, ys = get_batches(dataset, ‘train’, MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’])

# Calculate logits and loss using the model
logits, loss = model(xs, ys)

Чтобы обучить нашу базовую модель и отметить ее производительность, нам нужно указать некоторые параметры. Мы тренируем в общей сложности 1000 эпох. Увеличьте размер пакета с 8 до 32 и установите log_interval равным 10, указав, что код будет печатать или регистрировать сведения о ходе обучения каждые 10 пакетов. Для оптимизации воспользуемся оптимизатором Adam.# Update MASTER_CONFIG with training parameters
MASTER_CONFIG.update({
‘epochs’: 1000, # Number of training epochs
‘log_interval’: 10, # Log information every 10 batches during training
‘batch_size’: 32, # Increase batch size to 32
})

# Instantiate the SimpleBrokenModel with updated configuration
model = SimpleBrokenModel(MASTER_CONFIG)

# Define the Adam optimizer for model parameters
optimizer = torch.optim.Adam(
model.parameters(), # Pass the model parameters to the optimizer
)

Выполним процесс обучения и зафиксируем убыток из нашей базовой модели, включая общее количество параметров. Кроме того, каждая строка комментируется для наглядности:# Function to perform training
def train(model, optimizer, scheduler=None, config=MASTER_CONFIG, print_logs=False):
# Placeholder for storing losses
losses = []

# Start tracking time
start_time = time.time()

# Iterate through epochs
for epoch in range(config[‘epochs’]):
# Zero out gradients
optimizer.zero_grad()

# Obtain batches for training
xs, ys = get_batches(dataset, ‘train’, config[‘batch_size’], config[‘context_window’])

# Forward pass through the model to calculate logits and loss
logits, loss = model(xs, targets=ys)

# Backward pass and optimization step
loss.backward()
optimizer.step()

# If a learning rate scheduler is provided, adjust the learning rate
if scheduler:
scheduler.step()

# Log progress every specified interval
if epoch % config[‘log_interval’] == 0:
# Calculate batch time
batch_time = time.time() — start_time

# Evaluate loss on validation set
x = evaluate_loss(model)

# Store the validation loss
losses += [x]

# Print progress logs if specified
if print_logs:
print(f»Epoch {epoch} | val loss {x[‘val’]:.3f} | Time {batch_time:.3f} | ETA in seconds {batch_time * (config[‘epochs’] — epoch)/config[‘log_interval’] :.3f}»)

# Reset the timer
start_time = time.time()

# Print learning rate if a scheduler is provided
if scheduler:
print(«lr: «, scheduler.get_lr())

# Print the final validation loss
print(«Validation loss: «, losses[-1][‘val’])

# Plot the training and validation loss curves
return pd.DataFrame(losses).plot()

# Execute the training process
train(model, optimizer)

Начальная потеря кросс-энтропии до обучения составляет 4,17, а после 1000 эпох снижается до 3,93. В этом контексте кросс-энтропия отражает вероятность выбора неверного слова.

Наша модель включает в себя слой softmax на логитах, который преобразует вектор чисел в распределение вероятностей. Воспользуемся встроенной функцией F.cross_entropy, нам нужно напрямую передать ненормализованные логиты. Следовательно, мы модифицируем нашу модель соответствующим образом.# Modified SimpleModel class without softmax layer
class SimpleModel(nn.Module):
def __init__(self, config):

# Rest of the code

def forward(self, idx, targets=None):
# Embedding layer converts character indices to vectors
x = self.embedding(idx)

# Linear layers for modeling relationships between features
logits = self.linear(x)

# If targets are provided, calculate and return the cross-entropy loss
if targets is not None:

# Rest of the code

Давайте воссоздадим обновленную SimpleModel и обучим ее на 1000 эпох, чтобы наблюдать за любыми изменениями:# Create the updated SimpleModel
model = SimpleModel(MASTER_CONFIG)

# Obtain batches for training
xs, ys = get_batches(dataset, ‘train’, MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’])

# Calculate logits and loss using the model
logits, loss = model(xs, ys)

# Define the Adam optimizer for model parameters
optimizer = torch.optim.Adam(model.parameters())

# Train the model for 100 epochs
train(model, optimizer)

Уменьшив потери до 2,51, давайте рассмотрим, как наша языковая модель с примерно 33 000 параметров генерирует текст во время логического вывода. Мы создадим функцию ‘generate’, которую в дальнейшем будем использовать при репликации LLaMA:# Generate function for text generation using the trained model
def generate(model, config=MASTER_CONFIG, max_new_tokens=30):
idx = torch.zeros(5, 1).long()
for _ in range(max_new_tokens):
# Call the model
logits = model(idx[:, -config[‘context_window’]:])
last_time_step_logits = logits[
:, -1, :
] # all the batches (1), last time step, all the logits
p = F.softmax(last_time_step_logits, dim=-1) # softmax to get probabilities
idx_next = torch.multinomial(
p, num_samples=1
) # sample from the distribution to get the next token
idx = torch.cat([idx, idx_next], dim=-1) # append to the sequence
return [decode(x) for x in idx.tolist()]

# Generate text using the trained model
generate(model)

Сгенерированный текст выглядит не очень хорошо с нашей базовой моделью около 33 тысяч параметров. Однако теперь, когда мы заложили основу для этой простой модели, мы перейдем к построению архитектуры LLaMA в следующем разделе.

Репликация архитектуры LLaMA

В предыдущей части блога мы рассмотрели основные понятия, а теперь интегрируем эти понятия в нашу базовую модель. LLaMA представляет три архитектурные модификации оригинального Transformer:

  1. RMSNorm для предварительной нормализации
  2. Поворотные закладные
  3. Функция активации SwiGLU

Мы включим каждую из этих модификаций одну за другой в нашу базовую модель, повторяя и развивая их.

RMSNorm для предварительной нормализации:

Мы определяем функцию RMSNorm со следующими функциональными возможностями:class RMSNorm(nn.Module):
def __init__(self, layer_shape, eps=1e-8, bias=False):
super(RMSNorm, self).__init__()

# Registering a learnable parameter ‘scale’ as a parameter of the module
self.register_parameter(«scale», nn.Parameter(torch.ones(layer_shape)))

def forward(self, x):
«»»
Assumes shape is (batch, seq_len, d_model)
«»»
# Calculating the Frobenius norm, RMS = 1/sqrt(N) * Frobenius norm
ff_rms = torch.linalg.norm(x, dim=(1,2)) * x[0].numel() ** -.5

# Normalizing the input tensor ‘x’ with respect to RMS
raw = x / ff_rms.unsqueeze(-1).unsqueeze(-1)

# Scaling the normalized tensor using the learnable parameter ‘scale’
return self.scale[:x.shape[1], :].unsqueeze(0) * raw

мы определяем класс RMSNorm. Во время инициализации регистрируется параметр масштабирования. При прямом проходе он вычисляет норму Фробениуса входного тензора, а затем нормализует тензор. Наконец, тензор масштабируется по зарегистрированному параметру scale. Эта функция предназначена для использования в LLaMA для замены операции LayerNorm.

Теперь пришло время включить первую концепцию реализации LLaMA, которой является RMSNorm, в нашу простую модель NN. Вот обновленный код:# Define the SimpleModel_RMS with RMSNorm
class SimpleModel_RMS(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config

# Embedding layer to convert character indices to vectors
self.embedding = nn.Embedding(config[‘vocab_size’], config[‘d_model’])

# RMSNorm layer for pre-normalization
self.rms = RMSNorm((config[‘context_window’], config[‘d_model’]))

# Linear layers for modeling relationships between features
self.linear = nn.Sequential(
# Rest of the code

)

# Print the total number of model parameters
print(«Model parameters:», sum([m.numel() for m in self.parameters()]))

def forward(self, idx, targets=None):
# Embedding layer converts character indices to vectors
x = self.embedding(idx)

# RMSNorm pre-normalization
x = self.rms(x)

# Linear layers for modeling relationships between features
logits = self.linear(x)

if targets is not None:

# Rest of the code

Выполним модифицированную модель NN с помощью RMSNorm и посмотрим на обновленное количество параметров в модели вместе с потерями:# Create an instance of SimpleModel_RMS
model = SimpleModel_RMS(MASTER_CONFIG)

# Obtain batches for training
xs, ys = get_batches(dataset, ‘train’, MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’])

# Calculate logits and loss using the model
logits, loss = model(xs, ys)

# Define the Adam optimizer for model parameters
optimizer = torch.optim.Adam(model.parameters())

# Train the model
train(model, optimizer)

Потери при валидации немного снизились, и теперь параметры нашего обновленного LLM составляют около 55 000.

Поворотные закладные:

Далее мы реализуем поворотные позиционные внедрения. В RoPE авторы предлагают встраивать позицию токена в последовательность, поворачивая внедрение, применяя разное вращение в каждой позиции. Давайте создадим функцию, которая имитирует реальную бумажную реализацию RoPE:def get_rotary_matrix(context_window, embedding_dim):
# Initialize a tensor for the rotary matrix with zeros
R = torch.zeros((context_window, embedding_dim, embedding_dim), requires_grad=False)

# Loop through each position in the context window
for position in range(context_window):
# Loop through each dimension in the embedding
for i in range(embedding_dim // 2):
# Calculate the rotation angle (theta) based on the position and embedding dimension
theta = 10000. ** (-2. * (i — 1) / embedding_dim)
# Calculate the rotated matrix elements using sine and cosine functions
m_theta = position * theta
R[position, 2 * i, 2 * i] = np.cos(m_theta)
R[position, 2 * i, 2 * i + 1] = -np.sin(m_theta)
R[position, 2 * i + 1, 2 * i] = np.sin(m_theta)
R[position, 2 * i + 1, 2 * i + 1] = np.cos(m_theta)
return R

мы генерируем поворотную матрицу на основе указанного контекстного окна и измерения встраивания, следуя предложенной реализации RoPE.

Как вы, возможно, знакомы с архитектурой трансформеров, которая включает в себя головки внимания, нам аналогично нужно создавать головки внимания при репликации LLaMA. Для начала давайте создадим одну замаскированную голову внимания, используя функцию get_rotary_matrix, которую мы ранее разработали для поворотных внедрений. Кроме того, каждая строка комментируется для наглядности:class RoPEAttentionHead(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
# Linear transformation for query
self.w_q = nn.Linear(config[‘d_model’], config[‘d_model’], bias=False)
# Linear transformation for key
self.w_k = nn.Linear(config[‘d_model’], config[‘d_model’], bias=False)
# Linear transformation for value
self.w_v = nn.Linear(config[‘d_model’], config[‘d_model’], bias=False)
# Obtain rotary matrix for positional embeddings
self.R = get_rotary_matrix(config[‘context_window’], config[‘d_model’])

def get_rotary_matrix(context_window, embedding_dim):
# Generate rotational matrix for RoPE
R = torch.zeros((context_window, embedding_dim, embedding_dim), requires_grad=False)
for position in range(context_window):
for i in range(embedding_dim//2):

# Rest of the code

return R

def forward(self, x, return_attn_weights=False):
# x: input tensor of shape (batch, sequence length, dimension)

b, m, d = x.shape # batch size, sequence length, dimension

# Linear transformations for Q, K, and V
q = self.w_q(x)
k = self.w_k(x)
v = self.w_v(x)

# Rotate Q and K using the RoPE matrix
q_rotated = (torch.bmm(q.transpose(0, 1), self.R[:m])).transpose(0, 1)
k_rotated = (torch.bmm(k.transpose(0, 1), self.R[:m])).transpose(0, 1)

# Perform scaled dot-product attention
activations = F.scaled_dot_product_attention(
q_rotated, k_rotated, v, dropout_p=0.1, is_causal=True
)

if return_attn_weights:
# Create a causal attention mask
attn_mask = torch.tril(torch.ones((m, m)), diagonal=0)
# Calculate attention weights and add causal mask
attn_weights = torch.bmm(q_rotated, k_rotated.transpose(1, 2)) / np.sqrt(d) + attn_mask
attn_weights = F.softmax(attn_weights, dim=-1)
return activations, attn_weights

return activations

Теперь, когда у нас есть одна замаскированная голова внимания, которая возвращает весовые коэффициенты внимания, следующим шагом будет создание механизма внимания с несколькими головами.class RoPEMaskedMultiheadAttention(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
# Create a list of RoPEMaskedAttentionHead instances as attention heads
self.heads = nn.ModuleList([
RoPEMaskedAttentionHead(config) for _ in range(config[‘n_heads’])
])
self.linear = nn.Linear(config[‘n_heads’] * config[‘d_model’], config[‘d_model’]) # Linear layer after concatenating heads
self.dropout = nn.Dropout(.1) # Dropout layer

def forward(self, x):
# x: input tensor of shape (batch, sequence length, dimension)

# Process each attention head and concatenate the results
heads = [h(x) for h in self.heads]
x = torch.cat(heads, dim=-1)

# Apply linear transformation to the concatenated output
x = self.linear(x)

# Apply dropout
x = self.dropout(x)
return x

В оригинальной статье использовалось 32 головки для меньшего варианта 7b LLM, но из-за ограничений мы будем использовать 8 головок для нашего подхода.# Update the master configuration with the number of attention heads
MASTER_CONFIG.update({
‘n_heads’: 8,
})

Теперь, когда мы реализовали Rotational Embedding и Multi-head Attention, давайте перепишем нашу модель нейронной сети RMSNorm с обновленным кодом. Мы проверим его работоспособность, посчитаем потери и проверим количество параметров. Мы будем называть эту обновленную модель «RopeModel»class RopeModel(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config

# Embedding layer for input tokens
self.embedding = nn.Embedding(config[‘vocab_size’], config[‘d_model’])

# RMSNorm layer for pre-normalization
self.rms = RMSNorm((config[‘context_window’], config[‘d_model’]))

# RoPEMaskedMultiheadAttention layer
self.rope_attention = RoPEMaskedMultiheadAttention(config)

# Linear layer followed by ReLU activation
self.linear = nn.Sequential(
nn.Linear(config[‘d_model’], config[‘d_model’]),
nn.ReLU(),
)

# Final linear layer for prediction
self.last_linear = nn.Linear(config[‘d_model’], config[‘vocab_size’])

print(«model params:», sum([m.numel() for m in self.parameters()]))

def forward(self, idx, targets=None):
# idx: input indices
x = self.embedding(idx)

# One block of attention
x = self.rms(x) # RMS pre-normalization
x = x + self.rope_attention(x)

x = self.rms(x) # RMS pre-normalization
x = x + self.linear(x)

logits = self.last_linear(x)

if targets is not None:
loss = F.cross_entropy(logits.view(-1, self.config[‘vocab_size’]), targets.view(-1))
return logits, loss

else:
return logits

Давайте выполним модифицированную модель NN с RMSNorm, Rotational Embeddings и Masked Multi Head Attentions, чтобы увидеть обновленное количество параметров в модели, а также потери:# Create an instance of RopeModel (RMSNorm, RoPE, Multi-Head)
model = RopeModel(MASTER_CONFIG)

# Obtain batches for training
xs, ys = get_batches(dataset, ‘train’, MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’])

# Calculate logits and loss using the model
logits, loss = model(xs, ys)

# Define the Adam optimizer for model parameters
optimizer = torch.optim.Adam(model.parameters())

# Train the model
train(model, optimizer)

Потери при валидации снова немного снизились, и теперь параметры нашего обновленного LLM составляют около 55 000.

Давайте обучим модель на большее количество эпох, чтобы увидеть, продолжает ли уменьшаться потеря нашего воссозданного LLaMA LLM или нет.# Updating training configuration with more epochs and a logging interval
MASTER_CONFIG.update({
«epochs»: 5000,
«log_interval»: 10,
})

# Training the model with the updated configuration
train(model, optimizer)

Потери валидации продолжают снижаться, что позволяет предположить, что обучение для большего количества эпох может привести к дальнейшему снижению потерь, хотя и не значительно.

Функция активации SwiGLU:

Как уже упоминалось ранее, создатели LLaMA используют SwiGLU вместо ReLU, поэтому мы будем реализовывать уравнение SwiGLU в нашем коде.

class SwiGLU(nn.Module):
«»» Paper Link -> https://arxiv.org/pdf/2002.05202v1.pdf «»»
def __init__(self, size):
super().__init__()
self.config = config # Configuration information
self.linear_gate = nn.Linear(size, size) # Linear transformation for the gating mechanism
self.linear = nn.Linear(size, size) # Linear transformation for the main branch
self.beta = torch.randn(1, requires_grad=True) # Random initialization of the beta parameter

# Using nn.Parameter for beta to ensure it’s recognized as a learnable parameter
self.beta = nn.Parameter(torch.ones(1))
self.register_parameter(«beta», self.beta)

def forward(self, x):
# Swish-Gated Linear Unit computation
swish_gate = self.linear_gate(x) * torch.sigmoid(self.beta * self.linear_gate(x))
out = swish_gate * self.linear(x) # Element-wise multiplication of the gate and main branch
return out

После реализации уравнения SwiGLU на python нам нужно интегрировать его в нашу модифицированную языковую модель LLaMA (RopeModel).class RopeModel(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config

# Embedding layer for input tokens
self.embedding = nn.Embedding(config[‘vocab_size’], config[‘d_model’])

# RMSNorm layer for pre-normalization
self.rms = RMSNorm((config[‘context_window’], config[‘d_model’]))

# Multi-head attention layer with RoPE (Rotary Positional Embeddings)
self.rope_attention = RoPEMaskedMultiheadAttention(config)

# Linear layer followed by SwiGLU activation
self.linear = nn.Sequential(
nn.Linear(config[‘d_model’], config[‘d_model’]),
SwiGLU(config[‘d_model’]), # Adding SwiGLU activation
)

# Output linear layer
self.last_linear = nn.Linear(config[‘d_model’], config[‘vocab_size’])

# Printing total model parameters
print(«model params:», sum([m.numel() for m in self.parameters()]))

def forward(self, idx, targets=None):
x = self.embedding(idx)

# One block of attention
x = self.rms(x) # RMS pre-normalization
x = x + self.rope_attention(x)

x = self.rms(x) # RMS pre-normalization
x = x + self.linear(x) # Applying SwiGLU activation

logits = self.last_linear(x)

if targets is not None:
# Calculate cross-entropy loss if targets are provided
loss = F.cross_entropy(logits.view(-1, self.config[‘vocab_size’]), targets.view(-1))
return logits, loss

else:
return logits

Давайте выполним модифицированную модель NN с помощью RMSNorm, Rotational Embeddings, Masked Multi Head Attentions и SwiGLU, чтобы увидеть обновленное количество параметров в модели вместе с потерями:# Create an instance of RopeModel (RMSNorm, RoPE, Multi-Head, SwiGLU)
model = RopeModel(MASTER_CONFIG)

# Obtain batches for training
xs, ys = get_batches(dataset, ‘train’, MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’])

# Calculate logits and loss using the model
logits, loss = model(xs, ys)

# Define the Adam optimizer for model parameters
optimizer = torch.optim.Adam(model.parameters())

# Train the model
train(model, optimizer)

И снова потери валидации немного снизились, и теперь параметры нашего обновленного LLM составляют около 60 000.

На сегодняшний день мы успешно внедрили ключевые компоненты бумаги, а именно RMSNorm, RoPE и SwiGLU. Мы заметили, что эти внедрения привели к минимальному снижению потерь.

Теперь мы добавим слои в нашу LLaMA, чтобы изучить ее влияние на потери. В оригинальной бумаге использовалось 32 слоя для версии 7b, но мы будем использовать только 4 слоя. Давайте соответствующим образом настроим нашу модель.# Update model configurations for the number of layers
MASTER_CONFIG.update({
‘n_layers’: 4, # Set the number of layers to 4
})

Давайте начнем с создания одного слоя, чтобы понять его влияние.# add RMSNorm and residual connection
class LlamaBlock(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config

# RMSNorm layer
self.rms = RMSNorm((config[‘context_window’], config[‘d_model’]))

# RoPE Masked Multihead Attention layer
self.attention = RoPEMaskedMultiheadAttention(config)

# Feedforward layer with SwiGLU activation
self.feedforward = nn.Sequential(
nn.Linear(config[‘d_model’], config[‘d_model’]),
SwiGLU(config[‘d_model’]),
)

def forward(self, x):
# one block of attention
x = self.rms(x) # RMS pre-normalization
x = x + self.attention(x) # residual connection

x = self.rms(x) # RMS pre-normalization
x = x + self.feedforward(x) # residual connection
return x

Создайте экземпляр класса LlamaBlock и примените его к случайному тензору.# Create an instance of the LlamaBlock class with the provided configuration
block = LlamaBlock(MASTER_CONFIG)

# Generate a random tensor with the specified batch size, context window, and model dimension
random_input = torch.randn(MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’], MASTER_CONFIG[‘d_model’])

# Apply the LlamaBlock to the random input tensor
output = block(random_input)

Успешно создав один слой, мы можем использовать его для создания нескольких слоев. Кроме того, мы переименуем наш класс модели из «ropemodel» в «Llama», так как мы реплицировали каждый компонент языковой модели LLaMA.class Llama(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
# Embedding layer for token representations
self.embeddings = nn.Embedding(config[‘vocab_size’], config[‘d_model’])
# Sequential block of LlamaBlocks based on the specified number of layers
self.llama_blocks = nn.Sequential(
OrderedDict([(f»llama_{i}», LlamaBlock(config)) for i in range(config[‘n_layers’])])
)
# Feedforward network (FFN) for final output
self.ffn = nn.Sequential(
nn.Linear(config[‘d_model’], config[‘d_model’]),
SwiGLU(config[‘d_model’]),
nn.Linear(config[‘d_model’], config[‘vocab_size’]),
)

# Print total number of parameters in the model
print(«model params:», sum([m.numel() for m in self.parameters()]))

def forward(self, idx, targets=None):
# Input token indices are passed through the embedding layer
x = self.embeddings(idx)
# Process the input through the LlamaBlocks
x = self.llama_blocks(x)
# Pass the processed input through the final FFN for output logits
logits = self.ffn(x)

# If targets are not provided, return only the logits
if targets is None:
return logits
# If targets are provided, compute and return the cross-entropy loss
else:
loss = F.cross_entropy(logits.view(-1, self.config[‘vocab_size’]), targets.view(-1))
return logits, loss

Давайте выполним модифицированную модель LLaMA с RMSNorm, Rotational Embeddings, Masked Multi Head Attentions, SwiGLU и N_layers чтобы увидеть обновленное количество параметров в модели вместе с потерями:# Create an instance of RopeModel (RMSNorm, RoPE, Multi-Head, SwiGLU, N_layers)
llama = Llama(MASTER_CONFIG)

# Obtain batches for training
xs, ys = get_batches(dataset, ‘train’, MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’])

# Calculate logits and loss using the model
logits, loss = llama(xs, ys)

# Define the Adam optimizer for model parameters
optimizer = torch.optim.Adam(llama.parameters())

# Train the model
train(llama, optimizer)

Несмотря на то, что существует вероятность переобучения, очень важно изучить, приводит ли увеличение количества эпох к дальнейшему снижению потерь. Кроме того, обратите внимание, что наш текущий LLM имеет более 2 миллионов параметров.

Давайте обучим его на большее количество эпох.# Update the number of epochs in the configuration
MASTER_CONFIG.update({
‘epochs’: 10000,
})
# Train the LLaMA model for the specified number of epochs
train(llama, optimizer, scheduler=None, config=MASTER_CONFIG)

Убыток здесь составляет 1,08, мы можем добиться еще большего меньшего убытка, не сталкиваясь со значительным переобучением. Это говорит о том, что модель работает хорошо.

Давайте обучим модель еще раз, на этот раз включив планировщик# Training the model again, scheduler for better optimization.
train(llama, optimizer, config=MASTER_CONFIG)

До сих пор мы успешно реализовывали уменьшенную версию архитектуры LLaMA в нашем пользовательском наборе данных. Теперь давайте посмотрим на вывод, сгенерированный нашей языковой моделью с 2 миллионами параметров.# Generate text using the trained LLM (llama) with a maximum of 500 tokens
generated_text = generate(llama, MASTER_CONFIG, 500)[0]
print(generated_text)

Несмотря на то, что некоторые сгенерированные слова могут быть не идеальными на английском языке, наш магистр права с 2 миллионами параметров показал базовое понимание английского языка.

Теперь давайте посмотрим, насколько хорошо наша модель работает на тестовом наборе.# Get batches from the test set
xs, ys = get_batches(dataset, ‘test’, MASTER_CONFIG[‘batch_size’], MASTER_CONFIG[‘context_window’])

# Pass the test data through the LLaMA model
logits, loss = llama(xs, ys)

# Print the loss on the test set
print(loss)

Расчетный убыток на тестовом наборе составляет примерно 1,236.

Простой способ проверить наличие изменений в сгенерированных выходных данных — запустить обучение для большого количества эпох и наблюдать за результатами.

Эксперименты с гиперпараметрами

Настройка гиперпараметров — важнейший этап обучения нейронных сетей. В оригинальной статье Лама авторы использовали график обучения косинусному отжигу. Однако в наших экспериментах он показал себя не очень хорошо. Вот пример экспериментов с гиперпараметрами с использованием другого расписания обучения:# Update configuration
MASTER_CONFIG.update({
«epochs»: 1000
})

# Create Llama model with Cosine Annealing learning schedule
llama_with_cosine = Llama(MASTER_CONFIG)

# Define Adam optimizer with specific hyperparameters
llama_optimizer = torch.optim.Adam(
llama.parameters(),
betas=(.9, .95),
weight_decay=.1,
eps=1e-9,
lr=1e-3
)

# Define Cosine Annealing learning rate scheduler
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(llama_optimizer, 300, eta_min=1e-5)

# Train the Llama model with the specified optimizer and scheduler
train(llama_with_cosine, llama_optimizer, scheduler=scheduler)

Сохранение языковой модели (LLM)

Вы можете сохранить весь LLM или только параметры, используя следующее:# Save the entire model
torch.save(llama, ‘llama_model.pth’)

# If you want to save only the model parameters
torch.save(llama.state_dict(), ‘llama_model_params.pth’)

Чтобы сохранить модель PyTorch для библиотеки трансформеров Hugging Face, можно использовать метод save_pretrained. Вот пример:from transformers import GPT2LMHeadModel, GPT2Config

# Assuming Llama is your PyTorch model
llama_config = GPT2Config.from_dict(MASTER_CONFIG)
llama_transformers = GPT2LMHeadModel(config=llama_config)
llama_transformers.load_state_dict(llama.state_dict())

# Specify the directory where you want to save the model
output_dir = «llama_model_transformers»

# Save the model and configuration
llama_transformers.save_pretrained(output_dir)

GPT2Config используется для создания конфигурационного объекта, совместимого с GPT-2. Затем создается GPT2LMHeadModel и загружается вес из вашей модели ламы. Наконец, save_pretrained для сохранения модели и конфигурации в указанном каталоге.

Затем вы можете загрузить модель с помощью библиотеки Transformers:from transformers import GPT2LMHeadModel, GPT2Config

# Specify the directory where the model was saved
output_dir = «llama_model_transformers»

# Load the model and configuration
llama_transformers = GPT2LMHeadModel.from_pretrained(output_dir)

Заключение

В этом блоге мы рассмотрели пошаговый процесс реализации подхода LLaMA для создания собственной небольшой языковой модели (LLM). В качестве предложения рассмотрите возможность расширения модели примерно до 15 миллионов параметров, так как модели меньшего размера в диапазоне от 10 до 20 миллионов, как правило, лучше понимают английский язык. После того, как ваш магистр права овладеет языком, вы сможете настроить его для конкретных случаев использования.

Источник

Источник

Источник