Что матрица весов на самом деле делает с векторным пространством

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

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

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

Анализ представлений изучает, какую информацию кодируют внутренние векторы и как она организована. Механистическая интерпретируемость исследует, как компоненты сети используют эту информацию, определяя её поведение. Эти подходы пересекаются, но наличие информации в представлении ещё не доказывает, что сеть её использует.

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

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

Классификатор из одного нейрона

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

Начнём с набора данных, который можно разделить одной прямой. Представим четыре яблока одного сорта: A, B, C и D. Каждая точка в таблице ниже — одно яблоко, описанное двумя непрерывными измерениями: показателем цвета (x1x_1, от более зелёного к более красному) и мягкости (x2x_2, от более твёрдого к более мягкому). Метки указывают, неспелое яблоко (0) или спелое (1).

ЯблокоЦвет (x1x_1)Мягкость (x2x_2)Класс
A1-10.5-0.5Неспелое (0)
B0.5-0.5+0.5+0.5Неспелое (0)
C+0.5+0.5+0.5+0.5Спелое (1)
D+1+100Спелое (1)

Значения цвета и мягкости условные; они отцентрированы и масштабированы. Ноль соответствует опорному уровню каждого признака: отрицательные значения означают более зелёное или твёрдое яблоко, положительные — более красное или мягкое относительно этих уровней.

У нашей первой сети два входа и один выходной нейрон, без скрытого слоя.

Два входа и один выходной нейронЦвет и мягкость одного яблока поступают в один нейрон через веса w1 и w2. Он добавляет смещение и предсказывает класс 1 при положительной оценке, иначе — класс 0. Скрытого слоя нет.Входной векторВыходной нейронКлассx₁x₂w₁w₂Σ + bs > 0?0 или 1

Нейрон вычисляет оценку для каждого яблока, затем применяет порог, чтобы предсказать класс. Два веса нейрона можно записать в однострочную матрицу WW, а два входных значения яблока — в вектор-столбец x\mathbf{x}:

W=[w1w2],x=[x1x2]W=\begin{bmatrix}w_1&w_2\end{bmatrix}, \qquad \mathbf{x}=\begin{bmatrix}x_1\\x_2\end{bmatrix}

Чтобы вычислить оценку одного яблока, умножаем матрицу весов WW на его входной вектор x\mathbf{x} и добавляем смещение bb. Результат обозначаем s(x)s(\mathbf{x}): ss означает оценку (score), а x\mathbf{x} содержит измерения цвета и мягкости яблока:

s(x)=Wx+b=[w1w2]×[x1x2]+b=w1x1+w2x2+bs(\mathbf{x})=W\mathbf{x}+b =\begin{bmatrix}w_1&w_2\end{bmatrix}\times\begin{bmatrix}x_1\\x_2\end{bmatrix}+b =w_1x_1+w_2x_2+b

Здесь WxW\mathbf{x} вычисляет одно скалярное произведение двух векторов: вектора весов нейрона (w1,w2)(w_1,w_2), записанного строкой WW, и входного вектора яблока (x1,x2)(x_1,x_2). Умножаем каждый вес на соответствующий вход и складываем произведения. После добавления смещения получаем выходную оценку.

Умножение WxW\mathbf{x} уже является линейным преобразованием из R2\mathbb{R}^2 в R\mathbb{R}: из плоскости в прямую. Преобразование может превращать вектор в скаляр; число координат не обязано сохраняться. К геометрической интерпретации через базисные векторы мы вернёмся при построении скрытого слоя.

Эта модель — перцептрон, линейный классификатор из одного нейрона. Ступенчатая активация превращает оценку в предсказанную метку класса y^\hat y:

y^=step(s(x))={1если s(x)>0(спелое),0если s(x)0(неспелое)\hat y=\operatorname{step}(s(\mathbf{x}))= \begin{cases} 1 & \text{если }s(\mathbf{x})>0 \quad \text{(спелое)},\\ 0 & \text{если }s(\mathbf{x})\le0 \quad \text{(неспелое)} \end{cases}

При разметке данных мы выбрали 1 для спелых яблок и 0 для неспелых. Одного выходного нейрона достаточно: положительная оценка даёт метку 1, а нулевая или отрицательная — метку 0.

Теперь посмотрим, как нейрон классифицирует четыре яблока. В исходном 2D-виде цвет и мягкость каждого яблока определяют его положение на плоскости. Заливка показывает предсказанный класс для каждой возможной пары входов; прямая разделяет области неспелых и спелых яблок. Цвет маркера обозначает известную метку яблока, а розовое кольцо — ошибку предсказания.

Изменение весов и смещения меняет области решений, но яблоки остаются на месте. Мы подобрали измерения так, чтобы классы были линейно разделимы: при подходящих параметрах все четыре яблока попадают в правильные области.

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

s(x) = 1.00x₁ + 1.00x₂ − 0.50

4 из 4 классифицировано верно

1.00
1.00
-0.50
x₁-11x₂-11Яблоко A: Цвет -1, Мягкость -0.5, Оценка -2.00, Предсказание 0Яблоко B: Цвет -0.5, Мягкость 0.5, Оценка -0.50, Предсказание 0Яблоко C: Цвет 0.5, Мягкость 0.5, Оценка 0.50, Предсказание 1Яблоко D: Цвет 1, Мягкость 0, Оценка 0.50, Предсказание 1ABCD
Неспелое (0)Спелое (1)

Формула над графиком — вычисление оценки нейрона с подставленными текущими параметрами. Ползунки управляют весами w1w_1 и w2w_2 и смещением bb, показанными на схеме сети. Именно эти параметры определялись бы при обучении.

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

Разберём вычисление, задающее прямую в виджете. При начальных параметрах w1=1w_1=1, w2=1w_2=1 и b=0.5b=-0.5 оценка равна s(x)=x1+x20.5s(\mathbf{x})=x_1+x_2-0.5. Граница решения состоит из точек с нулевой оценкой:

x1+x20.5=0x_1+x_2-0.5=0

Это прямая. Например, для точки C с координатами (0.5,0.5)(0.5,0.5) подставляем в формулу x1=0.5x_1=0.5 и x2=0.5x_2=0.5: s(xC)=0.5+0.50.5=0.5s(\mathbf{x}_C)=0.5+0.5-0.5=0.5. Оценка положительная, значит, предсказание — спелое (1). Для точки B с координатами (0.5,0.5)(-0.5,0.5) подставляем x1=0.5x_1=-0.5 и x2=0.5x_2=0.5: s(xB)=0.5+0.50.5=0.5s(\mathbf{x}_B)=-0.5+0.5-0.5=-0.5. Оценка отрицательная, значит, предсказание — неспелое (0). Прямая делит всю плоскость на области этих двух ответов.

Можно также вычислить оценки всех четырёх яблок одним умножением матриц. Так обрабатывают пакет примеров, или батч: входные векторы собирают в матрицу XX, где каждое яблоко занимает строку, а цвет и мягкость — два столбца:

X=[10.50.50.50.50.510]X=\begin{bmatrix} -1&-0.5\\ -0.5&0.5\\ 0.5&0.5\\ 1&0 \end{bmatrix}

Параметры остаются прежними: два веса и одно смещение. Если начать с w1=1w_1=1, w2=1w_2=1 и b=0.5b=-0.5, как в виджете, матрица весов и её транспонированная версия будут такими:

W=[11],W=[11]W=\begin{bmatrix}1&1\end{bmatrix}, \qquad W^\top=\begin{bmatrix}1\\1\end{bmatrix}

Смещение b=0.5b=-0.5 хранится отдельно от WW и прибавляется к взвешенной сумме каждого яблока.

Для одного яблока в виде вектора-столбца мы использовали WxW\mathbf{x}. Когда яблоки записаны строками, используем XWXW^\top: транспонирование превращает строку весов нейрона в столбец, поэтому строка каждого яблока образует скалярное произведение с теми же весами. Добавляя к каждой оценке одно и то же смещение, получаем:

S=XW+b1=[10.50.50.50.50.510]×[11]+[0.50.50.50.5]=[20.50.50.5]S=XW^\top+b\mathbf{1} =\begin{bmatrix} -1&-0.5\\ -0.5&0.5\\ 0.5&0.5\\ 1&0 \end{bmatrix} \times\begin{bmatrix}1\\1\end{bmatrix} +\begin{bmatrix}-0.5\\-0.5\\-0.5\\-0.5\end{bmatrix} =\begin{bmatrix}-2\\-0.5\\0.5\\0.5\end{bmatrix}

Здесь 1\mathbf{1} — столбец из четырёх единиц, поэтому b1b\mathbf{1} повторяет смещение для всех яблок. Строки SS содержат оценки в порядке A–D. Для яблока A первая строка вычисляет 1(1)+1(0.5)0.5=21(-1)+1(-0.5)-0.5=-2 — точно так же, как при отдельной обработке. Ступенчатая функция даёт предсказания (0,0,1,1)(0,0,1,1), и все они верны. Обработка батчем не меняет вычисление оценки отдельного яблока. Наш батч содержит весь набор из четырёх примеров; большие наборы обычно делят на меньшие пакеты, как описано в статье о влиянии размера батча на обучение.

Четыре яблока, которые один нейрон не разделит

Продолжим с теми же четырьмя яблоками, описанными цветом (x1x_1) и мягкостью (x2x_2). Вручную изменим входные значения (x1,x2)(x_1,x_2), сохранив метки: A и B остаются неспелыми (0), а C и D — спелыми (1). Новые координаты помещают каждый класс на свою диагональ квадрата:

ЯблокоЦвет (x1x_1)Мягкость (x2x_2)Класс
A0.5-0.50.5-0.5Неспелое (0)
B+0.5+0.5+0.5+0.5Неспелое (0)
C0.5-0.5+0.5+0.5Спелое (1)
D+0.5+0.50.5-0.5Спелое (1)

Это знакомая конфигурация XOR. Мы сдвинули обычные двоичные координаты 0 и 1 к 0.5-0.5 и +0.5+0.5, расположив их по разные стороны нуля на каждой оси. Четыре точки образуют квадрат с центром в начале координат: яблоки с разными знаками входных значений получают метку «спелое» (1), с одинаковыми — «неспелое» (0).

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

Если пропустить обновлённый набор через исходный классификатор, прежние параметры w1=1w_1=1, w2=1w_2=1 и b=0.5b=-0.5 правильно классифицируют только одно яблоко из четырёх. Откройте настройки виджета и измените w2w_2 на 1-1: правильных ответов станет три, но C по-прежнему будет классифицировано неверно.

Настроить веса и смещение
1.00
1.00
-0.50

s(x) = 1.00x₁ + 1.00x₂ − 0.50

x₁-11x₂-11Яблоко A: Цвет -0.5, Мягкость -0.5, Оценка -1.50, Предсказание 0Яблоко C: Цвет -0.5, Мягкость 0.5, Оценка -0.50, Предсказание 0Яблоко D: Цвет 0.5, Мягкость -0.5, Оценка -0.50, Предсказание 0Яблоко B: Цвет 0.5, Мягкость 0.5, Оценка 0.50, Предсказание 1ABCD

Чтобы решить XOR, сохраним два входа и один выходной нейрон и добавим скрытый слой из двух нейронов с нелинейной активацией ReLU. Он преобразует измерения каждого яблока в новые координаты (h1,h2)(h_1,h_2), где выходной нейрон сможет разделить классы прямой:

Два входа, два скрытых нейрона и один выходной нейронОба входа связаны с обоими скрытыми нейронами через W. Каждый добавляет своё смещение и применяет ReLU, получая h1 или h2. Выходной нейрон объединяет их через V, добавляет c и предсказывает класс 1 при положительной оценке, иначе — класс 0.Входной векторСкрытый слойВыходной нейронWVx₁x₂h₁Σ + b₁ReLUh₂Σ + b₂ReLUΣ + cs > 0?Класс: 0 или 1

На рисунке показан многослойный перцептрон (MLP): сеть из полносвязных слоёв, где каждый нейрон получает все выходы предыдущего слоя. Это также сеть прямого распространения (FFN): информация идёт от входа к выходу без обратных петель. В других архитектурах используются другие типы слоёв. Свёрточные нейронные сети (CNN) применяют общие фильтры к локальным областям с помощью свёрточных слоёв и могут включать полносвязные слои, например для финальной классификации. Рекуррентные нейронные сети (RNN) используют слои, переносящие состояние между шагами последовательности.

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

Представляем яблоки векторами и матрицами

Каждое яблоко представлено вектором из двух чисел: показателей цвета и мягкости. Строчной буквой x\mathbf{x} обозначаем входной вектор одного яблока, содержащий цвет x1x_1 и мягкость x2x_2. Для яблока C запишем его столбцом:

xC=[0.50.5]R2\mathbf{x}_C = \begin{bmatrix} -0.5 \\ 0.5 \end{bmatrix} \in \mathbb{R}^2

R2\mathbb{R}^2 — пространство всех пар действительных чисел. Оно двумерно, потому что для определения любой точки достаточно двух независимых координатных направлений: движение вдоль первой оси, затем вдоль второй.

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

Яблоко C как вектор на входной плоскостиСтрелка идёт из начала координат к яблоку C с цветом −0.5 и мягкостью 0.5. Пунктир связывает её конец с этими значениями на осях. Стрелка и её конец представляют одну и ту же пару координат.Мягкость (x₂)Цвет (x₁)C(−0.5, 0.5)

Начнём пример на NumPy с представления яблока C:

import numpy as np

# Представление одного яблока: цвет и мягкость, в этом порядке.
x = np.array([-0.5, 0.5])        # Форма массива (2,)

В формулах мы записываем xC\mathbf{x}_C столбцом. В NumPy этот одномерный массив выступает входным вектором при умножении на WW.

Пространство представлений — вся плоскость. Наш набор содержит лишь четыре точки на ней. Четыре примера не делают пространство четырёхмерным: у каждого примера по-прежнему две координаты.

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

X=[0.50.50.50.50.50.50.50.5]R4×2X = \begin{bmatrix} -0.5 & -0.5 \\ 0.5 & 0.5 \\ -0.5 & 0.5 \\ 0.5 & -0.5 \end{bmatrix} \in \mathbb{R}^{4 \times 2}

Строки соответствуют примерам — нашим четырём яблокам; столбцы — признакам, цвету и мягкости. Метки спелости хранятся отдельно.

Векторы всех четырёх яблок на одной входной плоскостиЧетыре стрелки идут из начала координат к A (−0.5, −0.5), B (0.5, 0.5), C (−0.5, 0.5) и D (0.5, −0.5). Бирюзовые круги обозначают неспелые яблоки A и B, коралловые квадраты — спелые C и D. У всех четырёх векторов по две координаты.Мягкость (x₂)Цвет (x₁)A(−0.5, −0.5)B(0.5, 0.5)C(−0.5, 0.5)D(0.5, −0.5)

Это те же четыре входные точки, что и в виджете XOR. Стрелки явно показывают, что каждая точка представляет вектор из начала координат.

Матрица представлений может содержать векторы исходных данных, как XX здесь, или векторы, полученные от предыдущего слоя. В обоих случаях строка представляет один пример; между слоями меняется информация в координатах, а иногда и их количество. Это другая роль, чем у матрицы преобразования: XX хранит представления, а матрица весов WW задаёт применяемое к ним линейное преобразование.

У яблок уже есть два числовых измерения, поэтому составить входные векторы просто. Для других данных нужно решить, как представить пример вектором. Изображение в оттенках серого размером 28×2828 \times 28 можно превратить в вектор из 784 значений пикселей в фиксированном порядке, а токен — представить вектором вложения. Число координат такого вектора определяет число входов полносвязного слоя.

Нашему слою нужны два входа, даже если мы соберём ещё миллион примеров.

Как базисные векторы задают линейное преобразование

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

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

e1=[10],e2=[01]\mathbf{e}_1=\begin{bmatrix}1\\0\end{bmatrix}, \qquad \mathbf{e}_2=\begin{bmatrix}0\\1\end{bmatrix}

Они образуют стандартный базис входной плоскости. Записав их столбцами, получим единичную матрицу:

I=[e1  e2]=[1001]I=[\,\mathbf{e}_1\;\mathbf{e}_2\,] =\begin{bmatrix}1&0\\0&1\end{bmatrix}

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

x=[x1x2]\mathbf{x}=\begin{bmatrix}x_1\\x_2\end{bmatrix}

Первая координата x1x_1 масштабирует первый столбец e1\mathbf{e}_1, а вторая x2x_2 — второй столбец e2\mathbf{e}_2. Используем линейную комбинацию базисных векторов с коэффициентами x1x_1 и x2x_2, чтобы восстановить исходный вектор:

Ix=x1e1+x2e2линейная комбинация базисных векторов=x1[10]+x2[01]=[x1x2]=xI\mathbf{x}=\underbrace{x_1\mathbf{e}_1+x_2\mathbf{e}_2}_{\text{линейная комбинация базисных векторов}} =x_1\begin{bmatrix}1\\0\end{bmatrix} +x_2\begin{bmatrix}0\\1\end{bmatrix} =\begin{bmatrix}x_1\\x_2\end{bmatrix}=\mathbf{x}

Значит, для задания линейного преобразования можно выбрать новые координаты образов базисных векторов e1\mathbf{e}_1 и e2\mathbf{e}_2 и записать их в столбцы матрицы. Затем та же линейная комбинация с исходными координатами в качестве коэффициентов строит преобразованный вектор и показывает, куда он попадает в выходном пространстве.

Пусть линейное преобразование переводит e1\mathbf{e}_1 в [a,b][a,b]^\top, а e2\mathbf{e}_2 — в [c,d][c,d]^\top. Матрица преобразования записывает эти новые положения в свои столбцы:

M=[acbd],Me1=[ab],Me2=[cd]M=\begin{bmatrix}a&c\\b&d\end{bmatrix}, \qquad M\mathbf{e}_1=\begin{bmatrix}a\\b\end{bmatrix}, \qquad M\mathbf{e}_2=\begin{bmatrix}c\\d\end{bmatrix}

Как и выше, можно восстановить преобразованный входной вектор линейной комбинацией этих преобразованных направлений, взяв исходные координаты в качестве коэффициентов. Умножаем первый столбец MM на x1x_1, второй — на x2x_2 и складываем результаты:

M[x1x2]=x1[ab]+x2[cd]=[ax1+cx2bx1+dx2]M\begin{bmatrix}x_1\\x_2\end{bmatrix} =x_1\begin{bmatrix}a\\b\end{bmatrix} +x_2\begin{bmatrix}c\\d\end{bmatrix} =\begin{bmatrix}ax_1+cx_2\\bx_1+dx_2\end{bmatrix}

Входные координаты (x1,x2)(x_1,x_2) превратились в выходные координаты (ax1+cx2,  bx1+dx2)(ax_1+cx_2,\;bx_1+dx_2). Некоторые линейные преобразования сжимают плоскость в прямую или точку. Это тоже допустимые преобразования, и нейронные сети могут использовать их, чтобы отбрасывать информацию, сохраняя различия, полезные для задачи.

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

Матрица слоя задаёт преобразование

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

Матрица весов скрытого слоя WW — наша матрица преобразования: она задаёт линейное преобразование векторов входных данных или предыдущего слоя. Её форма зависит и от числа входов, и от числа нейронов: при dd входах и mm нейронах у WW будет mm строк и dd столбцов.

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

В нашем слое два нейрона и два входа, поэтому у WW две строки и два столбца. Каждая строка содержит вес одного нейрона для цвета (x1x_1) и мягкости (x2x_2).

W=[10.80.81]W = \begin{bmatrix} 1 & -0.8 \\ -0.8 & 1 \end{bmatrix}

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

Для применения преобразования умножаем входной вектор x\mathbf{x} на WW в порядке WxW\mathbf{x}. Это даёт по одной взвешенной сумме для каждого нейрона.

Помимо матрицы весов, у скрытого слоя есть вектор смещений b\mathbf{b} — по одному для каждого из двух нейронов. Добавление b\mathbf{b} даёт z=Wx+b\mathbf{z}=W\mathbf{x}+\mathbf{b}, вектор до активации. Умножение вместе с добавлением смещения образуют аффинное преобразование: линейное преобразование с последующим сдвигом. Один и тот же вектор смещения сдвигает все результаты. Линейное преобразование всегда переводит начало координат в начало координат; смещение переносит его в b\mathbf{b}. В нашем примере b=0\mathbf{b}=\mathbf{0}, поэтому сдвига нет и отображение остаётся линейным.

b=[00]\mathbf{b}=\begin{bmatrix}0\\0\end{bmatrix}

Затем ReLU заменяет отрицательные координаты z\mathbf{z} нулями, получая h=(h1,h2)\mathbf{h}=(h_1,h_2) — выход скрытого слоя. Это новое представление яблока, по которому выходной нейрон вычислит оценку. Полная последовательность такова:

x    W    Wx    +b    z    ReLU    h\mathbf{x} \;\xrightarrow{\;W\;}\; W\mathbf{x} \;\xrightarrow{\;+\mathbf{b}\;}\; \mathbf{z} \;\xrightarrow{\;\text{ReLU}\;}\; \mathbf{h}

Зададим ту же матрицу преобразования и смещение в NumPy:

# Две входные координаты -> две координаты до активации.
# Строки хранят веса нейронов; столбцы — образы входных базисных векторов.
W = np.array([[1.0, -0.8],
              [-0.8, 1.0]])     # Форма массива (2, 2)
b = np.array([0.0, 0.0])        # Вектор смещений; здесь сдвига нет

Для расчёта возьмём яблоко A, но виджет ниже позволяет проследить любое из четырёх яблок через ту же матрицу WW. Исходный вектор A:

xA=[0.50.5]\mathbf{x}_A=\begin{bmatrix}-0.5\\-0.5\end{bmatrix}

По правилу линейной комбинации столбцов вычислим WxAW\mathbf{x}_A пошагово, используя исходные координаты и столбцы WW.

  1. Столбцы WW — это образы базисных векторов e1\mathbf{e}_1 и e2\mathbf{e}_2. Первый столбец показывает, куда переходит e1=(1,0)\mathbf{e}_1=(1,0), второй — куда переходит e2=(0,1)\mathbf{e}_2=(0,1):

    W=[10.80.81],We1=[10.8],We2=[0.81].W=\begin{bmatrix}1&-0.8\\-0.8&1\end{bmatrix}, \qquad W\mathbf{e}_1=\begin{bmatrix}1\\-0.8\end{bmatrix}, \qquad W\mathbf{e}_2=\begin{bmatrix}-0.8\\1\end{bmatrix}.

    Это исходные базисные векторы после применения WW.

  2. Используем исходные координаты A как коэффициенты. Как мы показали выше, линейная комбинация преобразованных базисных векторов позволяет построить вектор A в преобразованном пространстве:

    WxA=0.5(We1)+(0.5)(We2)W\mathbf{x}_A=-0.5(W\mathbf{e}_1)+(-0.5)(W\mathbf{e}_2)

    Коэффициенты остаются прежними; меняются векторы, которые они умножают: теперь это преобразованные направления. Первая координата 0.5-0.5 масштабирует первый столбец WW — образ первого базисного вектора We1W\mathbf{e}_1. Вторая координата, тоже 0.5-0.5, масштабирует второй столбец — образ второго базисного вектора We2W\mathbf{e}_2.

    Полный расчёт выглядит так:

    WxA=0.5[10.8]+(0.5)[0.81]=[(0.5)×1(0.5)×(0.8)]+[(0.5)×(0.8)(0.5)×1]=[0.50.4]+[0.40.5]=[0.5+0.40.4+(0.5)]=[0.10.1]\begin{aligned} W\mathbf{x}_A &=-0.5\begin{bmatrix}1\\-0.8\end{bmatrix} +(-0.5)\begin{bmatrix}-0.8\\1\end{bmatrix}\\[6pt] &=\begin{bmatrix}(-0.5)\times 1\\(-0.5)\times(-0.8)\end{bmatrix} +\begin{bmatrix}(-0.5)\times(-0.8)\\(-0.5)\times 1\end{bmatrix}\\[6pt] &=\begin{bmatrix}-0.5\\0.4\end{bmatrix} +\begin{bmatrix}0.4\\-0.5\end{bmatrix} =\begin{bmatrix}-0.5+0.4\\0.4+(-0.5)\end{bmatrix} =\begin{bmatrix}-0.1\\-0.1\end{bmatrix} \end{aligned}

Два вклада частично компенсируются, оставляя (0.1,0.1)(-0.1,-0.1). Мы объединили преобразованные базисные направления с коэффициентами из исходных координат A. То же правило действует для B, у которого оба коэффициента равны +0.5+0.5:

WxB=0.5[10.8]+0.5[0.81]=[0.10.1]W\mathbf{x}_B =0.5\begin{bmatrix}1\\-0.8\end{bmatrix} +0.5\begin{bmatrix}-0.8\\1\end{bmatrix} =\begin{bmatrix}0.1\\0.1\end{bmatrix}

A и B приближаются к началу координат, но остаются разными точками. Матрица меняет их представления, не сливая их.

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

Выберите A, B, C или D, затем нажмите Преобразовать. Изначально выбрано A; его вектор перемещается из (0.5,0.5)(-0.5,-0.5) в (0.1,0.1)(-0.1,-0.1).

Яблоко:
-1-1110eeЯблоко A: (−0.50, −0.50)A
Исходные: (−0.50, −0.50)Сейчас: (−0.50, −0.50)

Действие преобразования хорошо видно на двух диагональных направлениях:

W[11]=0.2[11],W[11]=1.8[11]W\begin{bmatrix}1\\1\end{bmatrix} =0.2\begin{bmatrix}1\\1\end{bmatrix}, \qquad W\begin{bmatrix}1\\-1\end{bmatrix} =1.8\begin{bmatrix}1\\-1\end{bmatrix}

Направление A–B сжимается до одной пятой исходной длины, а C–D растягивается в 1.81.8 раза. Ни одно направление не исчезает. Форма квадрата из четырёх точек меняется, но диагонали классов по-прежнему пересекаются. Одно линейное преобразование ещё не сделало XOR разделимым. Для этого понадобится нелинейность ReLU, которую рассмотрим ниже.

Реализуем преобразование в NumPy с определённой выше матрицей W. Оператор @ выполняет матричное умножение:

x_A = np.array([-0.5, -0.5])    # Исходные координаты A

# Применяем W ко всему вектору.
transformed_A = W @ x_A
print(transformed_A)             # [-0.1 -0.1]

При умножении матрицы на вектор W @ x_A эквивалентно умножению каждого столбца W на соответствующую координату x_A и сложению полученных векторов. Запишем эту линейную комбинацию явно:

# W[:, 0] — первый столбец; W[:, 1] — второй.
transformed_A = x_A[0] * W[:, 0] + x_A[1] * W[:, 1]
print(transformed_A)             # [-0.1 -0.1]

Как нейроны вместе преобразуют вектор

Выше мы применили матрицу слоя к входному вектору яблока A:

W=[10.80.81],xA=[0.50.5]WxA=[0.10.1]W=\begin{bmatrix}1&-0.8\\-0.8&1\end{bmatrix}, \qquad \mathbf{x}_A=\begin{bmatrix}-0.5\\-0.5\end{bmatrix} \quad\longmapsto\quad W\mathbf{x}_A=\begin{bmatrix}-0.1\\-0.1\end{bmatrix}

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

В полносвязном слое каждый нейрон получает один и тот же входной вектор целиком. У яблока две координаты, поэтому каждому нейрону нужны два веса: для x1x_1 и для x2x_2. У нас два нейрона, каждый задаёт свою строку весов:

НейронВес для x1x_1Вес для x2x_2Строка WW
1110.8-0.8(1,0.8)(1,-0.8)
20.8-0.811(0.8,1)(-0.8,1)

Каждая строка содержит веса одного нейрона, а каждый столбец — образ одного входного базисного вектора. Например, первый столбец содержит веса обоих нейронов для x1x_1. Для входа e1=(1,0)\mathbf{e}_1=(1,0) они дают две выходные координаты We1=(1,0.8)W\mathbf{e}_1=(1,-0.8). Таким образом, столбец объединяет вклады обоих нейронов.

Собрав строки, получаем матрицу WW размером 2×22\times2. В общем случае для полносвязного слоя с dd входными координатами и mm нейронами:

WRm×d,xRd    WxRmW\in\mathbb{R}^{m\times d}, \qquad \mathbf{x}\in\mathbb{R}^d \;\longmapsto\; W\mathbf{x}\in\mathbb{R}^m

Размерность входа определяет число столбцов; число нейронов — число строк и выходных координат. Эти количества определяют форму матрицы. Значения весов определяют само линейное преобразование. Например, два нейрона с трёхмерным входом дают матрицу 2×32\times3, переводящую три входные координаты в две выходные.

Интересно, что линейная комбинация столбцов матрицы с входными координатами в качестве коэффициентов и скалярные произведения её строк с входным вектором дают в точности один и тот же преобразованный вектор.

В предыдущем разделе мы получили WxA=(0.1,0.1)W\mathbf{x}_A=(-0.1,-0.1), умножив столбцы на исходные координаты A и сложив их. Теперь получим тот же результат через вычисления нейронов: каждый берёт скалярное произведение своей строки весов и полного входного вектора, вычисляя одну выходную координату.

При фиксированных весах это скалярное произведение задаёт функцию, которая принимает вектор и возвращает одно скалярное число: fi(x)=wixf_i(\mathbf{x})=\mathbf{w}_i\cdot\mathbf{x}. Для первого нейрона такая скалярнозначная линейная функция имеет вид f1(x)=x10.8x2f_1(\mathbf{x})=x_1-0.8x_2 и отображает R2\mathbb{R}^2 в R\mathbb{R}. Матрицу преобразования и описываемую ею линейную часть слоя можно рассматривать как набор таких функций, по одной на нейрон. Их результаты вместе образуют T(x)=(f1(x),f2(x))=WxT(\mathbf{x})=(f_1(\mathbf{x}),f_2(\mathbf{x}))^\top=W\mathbf{x} — отображение из R2\mathbb{R}^2 в R2\mathbb{R}^2.

При нулевых смещениях результаты этих функций одновременно являются координатами до активации. Раскроем скалярные произведения:

z=Wx=[w1xw2x]=[1x1+(0.8)x2(0.8)x1+1x2]\mathbf{z}=W\mathbf{x} =\begin{bmatrix} \mathbf{w}_1\cdot\mathbf{x}\\ \mathbf{w}_2\cdot\mathbf{x} \end{bmatrix} =\begin{bmatrix} 1\cdot x_1+(-0.8)\cdot x_2\\ (-0.8)\cdot x_1+1\cdot x_2 \end{bmatrix}

Для того же яблока A оба нейрона получают (0.5,0.5)(-0.5,-0.5):

НейронВычислениеПолученная координата
11(0.5)+(0.8)(0.5)=0.11(-0.5)+(-0.8)(-0.5)=-0.1z1=0.1z_1=-0.1
2(0.8)(0.5)+1(0.5)=0.1(-0.8)(-0.5)+1(-0.5)=-0.1z2=0.1z_2=-0.1

Собрав эти два числа, получаем zA=(0.1,0.1)\mathbf{z}_A=(-0.1,-0.1) — тот же вектор, что и при линейной комбинации столбцов. Взвешенные суммы нейронов вместе выполняют матричное умножение слоя. Каждый нейрон даёт одну координату результата, а не применяет всю матрицу самостоятельно. Для каждого яблока используются одни и те же веса.

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

x1[10.8]+x2[0.81]комбинация столбцов=[x10.8x20.8x1+x2]=[(1,0.8)(x1,x2)(0.8,1)(x1,x2)]скалярные произведения строк\underbrace{ x_1\begin{bmatrix}1\\-0.8\end{bmatrix} +x_2\begin{bmatrix}-0.8\\1\end{bmatrix} }_{\text{комбинация столбцов}} =\begin{bmatrix}x_1-0.8x_2\\-0.8x_1+x_2\end{bmatrix} =\underbrace{ \begin{bmatrix} (1,-0.8)\cdot(x_1,x_2)\\ (-0.8,1)\cdot(x_1,x_2) \end{bmatrix} }_{\text{скалярные произведения строк}}

Покажем вычисления отдельных нейронов явно в NumPy:

# Каждая строка — вектор весов одного нейрона.
z1_A = np.dot(W[0], x_A)
z2_A = np.dot(W[1], x_A)
z_A = np.array([z1_A, z2_A])
print(z_A)                      # [-0.1 -0.1], тот же результат, что у W @ x_A

Добавление смещения делает функцию нейрона аффинной, а применение ReLU — нелинейной. Вместе выходы образуют скрытое представление h\mathbf{h}. Эффект ReLU рассмотрим ниже. Продолжая код прямого прохода для яблока C с ранее заданным входным вектором x, вычислим суммы обоих нейронов и добавим смещения одной операцией:

z = W @ x + b
print(z)                        # [-0.9  0.9] для яблока C

Если собрать все входные векторы в матрицу представлений, можно применить преобразование слоя ко всем четырём яблокам одним умножением матриц. Поместим A, B, C и D в строки XX, а их две входные координаты — в столбцы.

Для вектора-столбца мы использовали WxW\mathbf{x}. Поскольку в батче векторы записаны строками, используем XWXW^\top: транспонирование WW размещает веса каждого нейрона в столбце, чтобы строка яблока образовывала с ними скалярное произведение.

Транспонирование следует из способа хранения: примеры — строки XX, а веса нейронов — строки WW. Если бы веса хранились по одному нейрону в столбце, мы бы сразу использовали XWXW. Транспонирование согласует размерности, не меняя само преобразование.

При наших нулевых смещениях расчёт таков:

Z=XW=[0.50.50.50.50.50.50.50.5]×[10.80.81]=[0.10.10.10.10.90.90.90.9]Z=XW^\top =\begin{bmatrix} -0.5&-0.5\\ 0.5&0.5\\ -0.5&0.5\\ 0.5&-0.5 \end{bmatrix} \times \begin{bmatrix} 1&-0.8\\ -0.8&1 \end{bmatrix} =\begin{bmatrix} -0.1&-0.1\\ 0.1&0.1\\ -0.9&0.9\\ 0.9&-0.9 \end{bmatrix}

Каждая строка ZZ содержит преобразованный вектор одного яблока; каждый столбец — взвешенные суммы одного нейрона для всего батча. Первая строка для A по-прежнему равна (0.1,0.1)(-0.1,-0.1), как и при отдельном расчёте. Матрица WW симметрична, поэтому WW^\top содержит те же элементы, что и WW, но транспонирование выражает правильную операцию для примеров, записанных строками.

В NumPy с теми же W и b, определёнными выше:

# Один входной вектор на строку, в порядке A, B, C, D.
X = np.array([
    [-0.5, -0.5],
    [ 0.5,  0.5],
    [-0.5,  0.5],
    [ 0.5, -0.5],
])

# (4 яблока, 2 входа) @ (2 входа, 2 нейрона) -> (4, 2)
# Строка каждого яблока скалярно умножается на веса каждого нейрона.
Z = X @ W.T + b                  # Добавляем один и тот же вектор смещений к каждой строке
print(Z)
# [[-0.1 -0.1]   A
#  [ 0.1  0.1]   B
#  [-0.9  0.9]   C
#  [ 0.9 -0.9]]  D

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

Активация делает новое представление разделимым

Пока мы вычислили линейную часть скрытого слоя: каждый нейрон даёт одну координату z=Wx\mathbf{z}=W\mathbf{x} без сдвига, поскольку смещения нулевые. Координаты до активации:

ТочкаКлассz=Wx\mathbf{z}=W\mathbf{x}
A0(0.1,0.1)(-0.1,-0.1)
B0(0.1,0.1)(0.1,0.1)
C1(0.9,0.9)(-0.9,0.9)
D1(0.9,0.9)(0.9,-0.9)

Отрезок A–B стал короче, а C–D длиннее, но они всё ещё пересекаются. Прямая не может разделить два класса.

До W
-1-111x₁x₂ABCD
После W
-1-111z₁z₂ABCD

Масштаб обоих графиков одинаков. Отрезки соединяют точки одного класса и по-прежнему пересекаются в начале координат.

Скрытому слою осталось выполнить ещё одну операцию. Каждый нейрон применяет нелинейную активацию ReLU к своей взвешенной сумме. Вместе результаты образуют выходной вектор скрытого слоя h\mathbf{h}:

ReLU(z)=max(0,z),h=ReLU(z)\operatorname{ReLU}(z)=\max(0,z), \qquad \mathbf{h}=\operatorname{ReLU}(\mathbf{z})

Она сохраняет положительные значения и заменяет отрицательные нулями. Две скрытые координаты становятся такими:

h1=max(0,x10.8x2),h2=max(0,x20.8x1)h_1=\max(0,x_1-0.8x_2), \qquad h_2=\max(0,x_2-0.8x_1)

h1h_1 измеряет положительную часть x10.8x2x_1-0.8x_2, а h2h_2 — положительную часть x20.8x1x_2-0.8x_1. Это два признака, вычисляемых скрытым слоем.

Чтобы увидеть действие ReLU на представление яблока, выберите A, B, C или D и нажмите Применить ReLU. Виджет начинает с координат, полученных после WW. Изначально выбрано A, его вектор меняется с (0.1,0.1)(-0.1,-0.1) на (0,0)(0,0). Выбор другого яблока возвращает анимацию к его координатам до активации.

Яблоко:
-1-1110z₁z₂A
До ReLU: (−0.10, −0.10)Сейчас: (−0.10, −0.10)

Вот координаты всех четырёх яблок до и после ReLU, соответствующие виджету:

ТочкаКлассДо ReLU: z\mathbf{z}После ReLU: h\mathbf{h}
A0(0.1,0.1)(-0.1,-0.1)(0,0)(0,0)
B0(0.1,0.1)(0.1,0.1)(0.1,0.1)(0.1,0.1)
C1(0.9,0.9)(-0.9,0.9)(0,0.9)(0,0.9)
D1(0.9,0.9)(0.9,-0.9)(0.9,0)(0.9,0)

A попадает в начало координат, потому что обе координаты отрицательны. B остаётся в (0.1,0.1)(0.1,0.1), поскольку обе положительны. C и D теряют отрицательную координату и попадают на разные положительные полуоси. Отрезки классов больше не пересекаются.

В NumPy применим ReLU к матрице батча Z, вычисленной выше. Операция maximum сравнивает каждую координату с нулём и оставляет большее значение. Каждая строка H хранит скрытое представление одного яблока:

# Сохраняем положительные координаты; отрицательные заменяем нулями.
H = np.maximum(0, Z)
print(H)
# [[0.  0. ]   A
#  [0.1 0.1]   B
#  [0.  0.9]   C
#  [0.9 0. ]]  D

ReLU не ограничивает выход значениями 0 и 1. Здесь она сохраняет, например, 0.10.1 и 0.90.9. Только финальный порог классификации возвращает исключительно 0 или 1.

Выходной слой классифицирует скрытые представления

Скрытый слой создал новый вектор для каждого яблока. Выходной нейрон получает две его координаты, h1h_1 и h2h_2.

Два входа, два скрытых нейрона и один выходной нейронОба входа связаны с обоими скрытыми нейронами через W. Каждый добавляет своё смещение и применяет ReLU, получая h1 или h2. Выходной нейрон объединяет их через V, добавляет c и предсказывает класс 1 при положительной оценке, иначе — класс 0.Входной векторСкрытый слойВыходной нейронWVx₁x₂h₁Σ + b₁ReLUh₂Σ + b₂ReLUΣ + cs > 0?Класс: 0 или 1

У выходного нейрона свои веса в VV и своё смещение cc. При обучении эти параметры определялись бы вместе с весами и смещениями скрытого слоя. Для демонстрации выберем их вручную:

V=[11],c=0.5V=\begin{bmatrix}1&1\end{bmatrix}, \qquad c=-0.5

Его оценка — ещё одно скалярное произведение с последующим добавлением смещения:

s(h)=Vh+c=[11]×[h1h2]+(0.5)=1h1+1h20.5=h1+h20.5s(\mathbf{h})=V\mathbf{h}+c =\begin{bmatrix}1&1\end{bmatrix} \times\begin{bmatrix}h_1\\h_2\end{bmatrix}+(-0.5) =1\cdot h_1+1\cdot h_2-0.5 =h_1+h_2-0.5

Как и раньше, положительная оценка означает предсказание «спелое» (1), а нулевая или отрицательная — «неспелое» (0). Граница решения содержит представления с нулевой оценкой:

h1+h20.5=0h1+h2=0.5h_1+h_2-0.5=0 \quad\Longrightarrow\quad h_1+h_2=0.5

Этот порог лежит между суммами координат двух классов:

ЯблокоСкрытое представление (h1,h2)(h_1,h_2)h1+h2h_1+h_2ОценкаПредсказание
A(0,0)(0,0)000.5-0.5Неспелое (0)
B(0.1,0.1)(0.1,0.1)0.20.20.3-0.3Неспелое (0)
C(0,0.9)(0,0.9)0.90.90.40.4Спелое (1)
D(0.9,0)(0.9,0)0.90.90.40.4Спелое (1)

Для A и B суммы меньше 0.50.5, а для C и D — больше. Поэтому прямая h1+h2=0.5h_1+h_2=0.5 разделяет классы. Подобранный порог не единственный: любое значение строго между 0.20.2 и 0.90.9 также разделило бы эти четыре представления.

Посмотрим, как полная сеть классифицирует яблоки на исходной входной плоскости. Виджет открывается в режиме Вид сверху (2D): при наших параметрах все четыре яблока находятся в правильных областях. Переключитесь на Вид под углом (3D), чтобы увидеть (x1,x2,s(x))(x_1,x_2,s(\mathbf{x})) — исходные координаты каждого яблока и его итоговую оценку как высоту. Третья ось визуализирует скалярный выход; скрытое представление всё ещё имеет две координаты. Синяя поверхность оценок образует складки там, где скрытые нейроны переключаются между нулевым и положительным выходом. Её пересечение с входной плоскостью задаёт границу решения. Меняйте веса и смещения ползунками, чтобы исследовать поверхность и предсказания, или нажмите Сброс, чтобы восстановить наши параметры.

Настроить веса и смещения
Скрытый нейрон 1: h1 = ReLU(1.00x₁ + (-0.80)x₂ + (0.00))
1.00
-0.80
0.00
Скрытый нейрон 2: h2 = ReLU(-0.80x₁ + (1.00)x₂ + (0.00))
-0.80
1.00
0.00
Выход: s = 1.00h₁ + (1.00)h₂ + (-0.50)
1.00
1.00
-0.50

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

s(x) = Σⱼ vⱼ ReLU(wⱼ₁x₁ + wⱼ₂x₂ + bⱼ) + c

x₁-11x₂-11Яблоко A: Цвет -0.5, Мягкость -0.5, Оценка -0.50, Предсказание 0Яблоко B: Цвет 0.5, Мягкость 0.5, Оценка -0.30, Предсказание 0Яблоко C: Цвет -0.5, Мягкость 0.5, Оценка 0.40, Предсказание 1Яблоко D: Цвет 0.5, Мягкость -0.5, Оценка 0.40, Предсказание 1ABCD

В скрытом пространстве граница прямая, а в исходном — ломаная. A и B лежат в области неспелых яблок, а C и D — в области спелых по разные стороны от неё. При этих весах две спелые области соединяются дальше по положительной диагонали, за пределами четырёх примеров. Правильная классификация четырёх точек не определяет, что сеть должна предсказывать во всех остальных местах.

Чтобы реализовать выходной слой в NumPy, применим его веса и смещение ко всем четырём скрытым представлениям. При одном яблоке в каждой строке HH вычисляем S=HV+cS=HV^\top+c, добавляя к каждой оценке одно и то же смещение:

# Выходное преобразование: две скрытые координаты -> одна оценка.
V = np.array([[1.0, 1.0]])       # Форма массива (1, 2)
c = -0.5                        # Выходное смещение

# (4 яблока, 2 скрытые координаты) @ (2, 1) -> (4, 1)
# Каждая строка скалярно умножается на одни и те же выходные веса.
S = H @ V.T + c
predictions = (S > 0).astype(int)  # 1 = спелое, 0 = неспелое

for apple, score, prediction in zip("ABCD", S[:, 0], predictions[:, 0]):
    print(f"{apple}: score={score:.1f}, prediction={prediction}")
# A: score=-0.5, prediction=0
# B: score=-0.3, prediction=0
# C: score=0.4, prediction=1
# D: score=0.4, prediction=1

Почему одной ReLU недостаточно для этого набора

Что, если скрытый слой использует единичную матрицу II и нулевые смещения? Он вычислит h=ReLU(Ix)=ReLU(x)\mathbf{h}=\operatorname{ReLU}(I\mathbf{x})=\operatorname{ReLU}(\mathbf{x}) — то же самое, что ReLU, применённая непосредственно к исходным входам. Получится всё та же конфигурация XOR: отрицательные координаты станут нулями, и A окажется в (0,0)(0,0), B — в (0.5,0.5)(0.5,0.5), C — в (0,0.5)(0,0.5), D — в (0.5,0)(0.5,0). Это вершины меньшего квадрата, где классы по-прежнему занимают разные диагонали. Прямой их не разделить.

Переключайтесь между Исходные, ReLU(x) и ReLU(Wx), чтобы сравнить три конфигурации. Во втором случае веса задаёт единичная матрица, в третьем — выбранная нами WW. Оба варианта скрытого слоя начинают с исходных входных векторов и используют нулевые смещения.

Исходные входы: диагонали двух классов пересекаются.

-1-1-0.5-0.500.50.511x₁x₂A: (−0.5, −0.5), Метка 0AB: (0.5, 0.5), Метка 0BC: (−0.5, 0.5), Метка 1CD: (0.5, −0.5), Метка 1D

Каждая клетка имеет размер 0.1 × 0.1. Во всех трёх видах масштаб осей одинаков.

A: (−0.5, −0.5)B: (0.5, 0.5)C: (−0.5, 0.5)D: (0.5, −0.5)

Ни одна прямая не разделяет эти два класса.

Матрица WW сначала меняет координаты так, чтобы ReLU действовала на более полезную конфигурацию. Она помещает B в (0.1,0.1)(0.1,0.1), C и D — в (0.9,0.9)(-0.9,0.9) и (0.9,0.9)(0.9,-0.9). Затем ReLU оставляет B рядом с началом координат, переносит C и D дальше по положительным полуосям, а A — в начало координат. Эти представления уже разделяются прямой.

Ни сама матрица WW, ни ReLU непосредственно на исходных входах не решает этот пример; именно их сочетание делает классы линейно разделимыми. Как мы видели, выходной слой способен разделить полученные представления.

Слои образуют композицию функций

Некоторые книги описывают нейронные сети как композицию функций, отображающих векторы из одного векторного пространства в другое. Наш скрытый слой отображает R2\mathbb{R}^2 в R2\mathbb{R}^2, создавая скрытое представление, а выходной слой — R2\mathbb{R}^2 в R\mathbb{R}, создавая скалярную оценку. Каждая функция получает результат предыдущей. Эти функции могут быть линейными или нелинейными, как матрица весов и ReLU.

Обозначим функцию скрытого слоя нашей сети через FF, а выходного — через GG:

F(x)=ReLU(Wx+b),G(h)=Vh+cF(\mathbf{x})=\operatorname{ReLU}(W\mathbf{x}+\mathbf{b}), \qquad G(\mathbf{h})=V\mathbf{h}+c

Прямой проход, который мы проследили в виджетах, принимает вид

xFhGs(x)\mathbf{x}\xrightarrow{F}\mathbf{h}\xrightarrow{G}s(\mathbf{x})

Подстановка выхода скрытого слоя в функцию выходного даёт

s(x)=G(F(x))=V×ReLU(Wx+b)+cs(\mathbf{x})=G(F(\mathbf{x})) =V\times\operatorname{ReLU}(W\mathbf{x}+\mathbf{b})+c

Это и есть композиция функций в нашем случае. Её обычная запись — GFG\circ F: сначала применить FF, затем GG. Более глубокая сеть следует тому же принципу FLF2F1F_L\circ\cdots\circ F_2\circ F_1: каждый слой преобразует полученное представление и передаёт результат следующему.

Без нелинейных активаций слои сводятся к одному аффинному отображению

Возможно, вы читали, что без нелинейных активаций все слои сводятся к одному. Это легко увидеть на наших двух матрицах весов. Временно уберём ReLU. Поскольку смещения скрытого слоя нулевые, выходной нейрон напрямую получает WxW\mathbf{x}:

sбез ReLU(x)=V(Wx)+c=(VW)x+cs_{\text{без ReLU}}(\mathbf{x}) =V(W\mathbf{x})+c =(VW)\mathbf{x}+c

Можно сначала перемножить VV и WW и использовать результат как единую матрицу весов:

VW=[11]×[10.80.81]=[11+1(0.8)1(0.8)+11]=[0.20.2]VW= \begin{bmatrix}1&1\end{bmatrix} \times\begin{bmatrix}1&-0.8\\-0.8&1\end{bmatrix} =\begin{bmatrix}1\cdot1+1\cdot(-0.8)&1\cdot(-0.8)+1\cdot1\end{bmatrix} =\begin{bmatrix}0.2&0.2\end{bmatrix}

Полученная сеть вычисляет

sбез ReLU(x)=0.2x1+0.2x20.5s_{\text{без ReLU}}(\mathbf{x})=0.2x_1+0.2x_2-0.5

Мы вернулись к оценке того же вида, что у исходного одиночного нейрона: два взвешенных входа и смещение, с прямой границей решения. Эти веса дают для A, B, C и D оценки 0.7-0.7, 0.3-0.3, 0.5-0.5 и 0.5-0.5, предсказывая «неспелое» для всех. Другие веса могли бы улучшить результат, но ни одна аффинная оценка не классифицирует эту конфигурацию XOR без ошибок.

Сведение относится к вычисляемой функции: один аффинный слой способен дать точно те же выходы, что и вся последовательность. Ненулевые смещения не меняют вывода. Они объединяются в одно смещение:

V(Wx+b)+c=(VW)x+(Vb+c)V(W\mathbf{x}+\mathbf{b})+c =(VW)\mathbf{x}+(V\mathbf{b}+c)

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

Умножение матриц записывает композицию преобразований

Это напрямую связано со столбцами как образами входных базисных векторов. Столбцы WW показывают, куда переходят e1\mathbf{e}_1 и e2\mathbf{e}_2 после первого преобразования. Применение VV к этим образам показывает, куда они попадут после обоих:

VW=[V(We1)V(We2)]=[0.20.2]VW=\begin{bmatrix}V(W\mathbf{e}_1)&V(W\mathbf{e}_2)\end{bmatrix} =\begin{bmatrix}0.2&0.2\end{bmatrix}

Каждый входной базисный вектор попадает в 0.20.2 на выходной числовой прямой. Для любого входа то же правило линейной комбинации столбцов даёт (VW)x=0.2x1+0.2x2(VW)\mathbf{x}=0.2x_1+0.2x_2.

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

xM1M1xM2M2(M1x)=(M2M1)x\mathbf{x}\xrightarrow{M_1}M_1\mathbf{x} \xrightarrow{M_2}M_2(M_1\mathbf{x}) =(M_2M_1)\mathbf{x}

Первой действует крайняя правая матрица. Произведение представляет оба линейных преобразования как одно.

ReLU меняет то, что получает следующий слой

Вернём ReLU между WW и VV. Выходной нейрон получает ReLU(Wx)\operatorname{ReLU}(W\mathbf{x}), поэтому складывает уже не сами координаты после WW, а их неотрицательные части:

s(x)=max(0,x10.8x2)+max(0,x20.8x1)0.5s(\mathbf{x}) =\max(0,x_1-0.8x_2)+\max(0,x_2-0.8x_1)-0.5

Например, координаты C после WW равны (0.9,0.9)(-0.9,0.9). Без ReLU они взаимно уничтожаются, давая оценку 0.5-0.5. С ReLU выходной нейрон получает (0,0.9)(0,0.9) и даёт оценку 0.40.4. Те же выходные веса теперь предсказывают правильный класс, потому что получают другое представление.

VReLU(Wx)(VW)xв общем случаеV\operatorname{ReLU}(W\mathbf{x})\neq(VW)\mathbf{x} \quad\text{в общем случае}

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

Это связывает два вида нашего виджета классификатора. В скрытом пространстве граница — прямая h1+h2=0.5h_1+h_2=0.5. Подставив функции скрытого слоя, получим ту же границу в исходных координатах:

max(0,x10.8x2)+max(0,x20.8x1)=0.5\max(0,x_1-0.8x_2)+\max(0,x_2-0.8x_1)=0.5

Там, где активен только первый нейрон, выражение сводится к x10.8x2=0.5x_1-0.8x_2=0.5. Там, где только второй, — к x20.8x1=0.5x_2-0.8x_1=0.5. Когда активны оба, получаем 0.2(x1+x2)=0.50.2(x_1+x_2)=0.5. Эти прямолинейные части соединяются под углом и образуют границу, которую мы видим на входной плоскости.

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

Запускаем модель для четырёх яблок в NumPy

Объединим операции в один запускаемый пример на NumPy. На всём прямом проходе каждая строка остаётся одним яблоком:

X4×2    Z4×2    ReLU    H4×2    S4×1\underbrace{X}_{4\times2} \;\longrightarrow\; \underbrace{Z}_{4\times2} \;\xrightarrow{\;\operatorname{ReLU}\;}\; \underbrace{H}_{4\times2} \;\longrightarrow\; \underbrace{S}_{4\times1}

Используем прежний способ записи, по одному примеру в строке: Z=XW+bZ=XW^\top+\mathbf{b}, H=ReLU(Z)H=\operatorname{ReLU}(Z) и S=HV+cS=HV^\top+c, добавляя каждое смещение ко всем строкам. Размер батча и размер представления — разные измерения: примеров остаётся четыре, а две скрытые координаты каждого яблока превращаются в одну оценку.

import numpy as np

# Матрица представлений X размера (4, 2):
# строки — яблоки A, B, C, D; столбцы — цвет и мягкость.
X = np.array([
    [-0.5, -0.5],
    [ 0.5,  0.5],
    [-0.5,  0.5],
    [ 0.5, -0.5],
])
labels = np.array([0, 0, 1, 1])   # Целевые метки хранятся отдельно от X

# Матрица преобразования W размера (2, 2), общая для всех яблок.
# Строки хранят веса нейронов; столбцы — преобразованные базисные векторы.
W = np.array([
    [ 1.0, -0.8],
    [-0.8,  1.0],
])
b = np.array([0.0, 0.0])

# Выходное преобразование V размера (1, 2): две координаты -> одна оценка.
V = np.array([[1.0, 1.0]])
c = -0.5

# Примеры записаны строками, поэтому транспонируем W для вычисления
# скалярного произведения каждого яблока с весами каждого нейрона: (4, 2) @ (2, 2).
# NumPy добавляет один и тот же вектор смещений к каждой строке.
Z = X @ W.T + b                  # Форма массива (4, 2)

# ReLU заменяет отрицательные координаты нулями.
H = np.maximum(0, Z)             # Форма массива (4, 2): новые представления

# Композиция выходного и скрытого преобразований для каждой строки:
# (4, 2) @ (2, 1) -> (4, 1), затем добавляем одно и то же скалярное смещение.
scores = H @ V.T + c             # Одна оценка на яблоко
predictions = (scores[:, 0] > 0).astype(int)

print(H)
# [[0.  0. ]   A
#  [0.1 0.1]   B
#  [0.  0.9]   C
#  [0.9 0. ]]  D

print(scores[:, 0])             # [-0.5 -0.3  0.4  0.4]
print(predictions)              # [0 0 1 1]

XX и HH описывают те же примеры в разных пространствах. WW и VV задают преобразования между ними. Активация между ними позволяет полной сети выразить нелинейное правило.

Почему изменение представления помогает

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

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

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

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

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