Об одной нерассмотренной возможности загрузки сознания, или что такое самообучающаяся анимация

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Попыток "загрузить" сознание в компьютер известно великое множество, однако все они страдают хотя бы от одного из двух больших недостатков:

  • Невозможность выразить эмоции и субъективную составляющую психики.

  • Страшная дороговизна и ресурсоёмкость.

Рассмотрим, к примеру, генеративные модели, создающие тексты, стилизованные под определённого автора: просто RNN, дополняющую "затравку", скормленную ей, VAE и WGAN-GP5, генерирующие тексты из нормального распределения. Они, конечно, могут подражать даже стилю мышления авторов, однако не похоже, чтобы они могли иметь свою эмоциональную жизнь, судя по их "продукции". Также существуют более "дотошные" модели, типа GPT-3, способные даже поддержать диалог, однако они тоже по понятным причинам, мягко говоря, слегка отдают некоторой бесчувственностью, к тому же, как я уже сказал, они страшно ресурсоёмки.

Моё решение данно проблемы состоит в том, чтобы использовать в качестве модели не текст, а видео, причём не простое, а генерирующееся прямо по ходу обучения нейросети. Я взял свёрточную VAE, состоящую из свёртки-развёртки 10 на 10 с такими же страйдами, а между свёрткой и развёрткой поставил 40 слоёв свёртки 3 на 3 с Gated Linear Unit, то есть с умножением одной половины каналов на другую, пропущенную через сигмоиду (всё, разумеется, поэлементно), а также Group Normalization:

class GaussianEncoder(Module):
    def __init__(self):
        super(GaussianEncoder, self).__init__()
        self.gn1 = GroupNorm(2, 128)
        self.cnn1 = Conv2d(3, 128, kernel_size=(10, 10), stride=(10, 10))
        self.cnn = torch.nn.ModuleList()
        self.gn = torch.nn.ModuleList()
        for i in range(0, 20):
            self.cnn.append(Conv2d(64, 128, kernel_size=(3, 3), padding=(1, 1)))
            self.gn.append(GroupNorm(2, 128))
        self.cnn_out = Conv2d(64, 8, kernel_size=(1, 1))
        self.glu = GLU(dim=1)

    def forward(self, x):
        y = x
        y = self.cnn1(y)
        y = self.gn1(y)
        y = self.glu(y)
        for i in range(0, 20):
            y_ = self.cnn[i](y)
            y_ = self.gn[i](y_)
            y_, y_g = torch.chunk(y_, 2, dim=1)
            y_g = y_g.sigmoid()
            y = y_ * (1.0 - y_g) + y * y_g
        y = self.cnn_out(y)
        return y


class GaussianDecoder(Module):
    def __init__(self):
        super(GaussianDecoder, self).__init__()
        self.cnn_in = Conv2d(8, 128, kernel_size=(1, 1))
        self.gn_in = GroupNorm(2, 128)
        self.gn = torch.nn.ModuleList()
        self.cnn = torch.nn.ModuleList()
        self.gn5 = torch.nn.GroupNorm(2, 6)
        self.cnn5 = ConvTranspose2d(64, 6, kernel_size=(10, 10), stride=(10, 10))
        for i in range(0, 20):
            self.gn.append(GroupNorm(2, 128))
            self.cnn.append(Conv2d(64, 128, kernel_size=(3, 3), padding=(1, 1)))
        self.glu = GLU(dim=1)

    def forward(self, x):
        y = x
        y = self.cnn_in(y)
        y = self.gn_in(y)
        y = self.glu(y)
        for i in range(0, 20):
            y_ = self.cnn[i](y)
            y_ = self.gn[i](y_)
            y_, y_g = torch.chunk(y_, 2, dim=1)
            y_g = y_g.sigmoid()
            y = y_ * (1.0 - y_g) + y * y_g
        y = self.cnn5(y)
        y = self.gn5(y)
        y = self.glu(y)
        return y


class GaussianAutoencoder(Module):
    def __init__(self):
        super(GaussianAutoencoder, self).__init__()
        self.encoder = GaussianEncoder()
        self.encoder_var = GaussianEncoder()
        self.decoder = GaussianDecoder()
        self.sigma = torch.nn.Parameter(torch.randn(1))
        self.loss_fn = torch.nn.MSELoss(reduction='none')

    def forward(self, x):
        y = self.encoder(x)
        y_var = self.encoder_var(x).sigmoid()
        if self.training:
            z = torch.distributions.Normal(y, y_var)
            norm = Normal(0.0, 1.0)
            y = z.rsample()
            y = self.decoder(y)
            t = Normal(y, (self.sigma.sigmoid() * 2.0 + 1e-6).sqrt())
            y = t.rsample()
            loss = self.loss_fn(y, x) / (2.0 * (self.sigma.sigmoid() * 2.0 + 1e-6))
            loss = loss.mean(dim=0).sum() + div(z, norm).mean(dim=0).sum()
            loss = loss + (self.sigma.sigmoid() * 2.0 + 1e-6).log() * 360.0 * 640.0 * 3.0 * 0.5
            return y, loss
        y = self.decoder(y)
        return y
    def encode(self, x):
        y = self.encoder(x)
        y_var = self.encoder_var(x).sigmoid()
        z = Normal(y, y_var)
        y = z.sample()
        return y

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

Далее я обучаю этот автоэнкодер на данных трёх картинках:

Но и это ещё не всё: я сохраняю автоэнкодер, и затем создаю "рекодер", манипулирующий кодами изображений, представляющими собой feature maps с 8 каналами размерностью 36 на 64 (картинки я интерполировал заранее в размер 360 на 640):

def l2norm(x):
    y = x * x
    y = y.sum(dim=1, keepdim=True).sqrt()
    y = x / y
    return y

class Recoder(Module):
    def __init__(self, ):
        super(Recoder, self).__init__()
        self.cnn_in = Conv2d(8, 64, kernel_size=(1, 1))
        self.cnn = torch.nn.ModuleList()
        for i in range(0, 20):
            self.cnn.append(Conv2d(64, 128, kernel_size=(3, 3), padding=(1, 1)))
        self.cnn_out = Conv2d(64, 8, kernel_size=(1, 1))

    def forward(self, x, return_h=False):
        y = x
        h = []
        y = self.cnn_in(y)
        for i in range(0, 20):
            y_ = self.cnn[i](y)
            y_, y_g = torch.chunk(y_, 2, dim=1)
            y_g = y_g.sigmoid()
            y = y_ * (1.0 - y_g) + y * y_g
            y = l2norm(y)
            h.append(y)
        y = self.cnn_out(y)
        if return_h:
            return y, h
        return y

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

  def corrloss(y, x):
      zx = ((x - x.mean()) ** 2 + 1e-16).sum()
      zy = ((y - y.mean()) ** 2 + 1e-16).sum()
      z = 1.0 - ((x - x.mean()) * (y - y.mean()) / (zx * zy).sqrt()).sum()
      r = (zx.log() - zy.log()) ** 2
      r = r + (x.mean() - y.mean()) ** 2
      return z, r
	t = float(i % 5300 + 1) / 5300.0
	s1 = 1.0 - 1.0 / (2.0 ** (150.0 * (t - 0.1)) + 1.0)
	s2 = 1.0 - 1.0 / (2.0 ** (150.0 * (0.9 - t)) + 1.0)
	s_ = 1.0 / (1.0 + ((t - 0.9) * 100.0) ** 2.0)
	s_ = s_ + 1.0 / (1.0 + ((t - 0.93) * 100.0) ** 2.0)
	s_ = s_ + 1.0 / (1.0 + ((t - 0.96) * 100.0) ** 2.0)
	s = ((s1 + s2 - 1.0) * 0.16 + s_ / 0.8)
	y_, h = rec(x1, True)
	y_ = y_.tanh() * inp_c_max * 1.1
	loss1, _ = corrloss(y_, x1)
	if i % 5300 < 1060:
		  loss2, r = corrloss(y_, inp_code)
	else:
		  loss2, r = corrloss(y_, inp_code2)
	loss = loss1 * (loss1 - s).detach()
	loss = loss + loss2 * loss2.detach() * (1.0 - 0.8 * s) + 0.25 * r

inp_code и inp_code2 - это коды первой и третьей картинок, s1 и s2 отвечают за "плато" свидания ручки с колпачком, а s_ - за его кульминацию. Также я использую в данном случае по отношению к коду картинки метод скользящего среднего:

x1 = x1 * 0.5 + 0.5 * y_

А инициализирую я этот код случайными числами из нормального распределения:

with torch.no_grad():
	triangular = (1.0 - (torch.arange(100).float().unsqueeze(0) - 50.0).abs() / 50.0) / 100.0
	inp_code = autenc1.encode(inp)
	inp_code2 = autenc1.encode(inp2)
	inp_c_max = max([inp_code.abs().max(), inp_code2.abs().max()])
	x1 = torch.randn(inp_code.size())
	m = (torch.rand(20) * 36.0).long()
	n = (torch.rand(20) * 64.0).long()
	q = (torch.rand(20) * 16.0).long()
	p = (torch.rand(20) * 20.0).long()

triangular, в данном случае, - это треугольное окно для сглаживания спектра "энцефалограммы" рекодера, а сама же "ЭЭГ" нормируется по принципу всё того же скользящего среднего:

  from PIL import Image

  from matplotlib import pyplot
  from scipy.fft import dct
  
  h_var = [1.0] * 100
  h_mu = [0.0] * 100

	with torch.no_grad():
		h1_ = []
		for j in range(0, 20):
			h_ = (h[p[j]][:, q[j], m[j], n[j]] - h_mu[j]) / h_var[j] ** 0.5
			h_mu[j] = h_mu[j] * 0.99 + h[p[j]][:, q[j], m[j], n[j]] * 0.01
			h_var[j] = h_var[j] * 0.9 + 0.1 * (h[p[j]][:, q[j], m[j], n[j]] - h_mu[j]) ** 2
			h1_.append(h_.unsqueeze(0))

		h1_ = torch.cat(h1_, dim=0)
		h1.append(h1_)
		if (i + 1) % 1000 == 0:
			h1 = torch.cat(h1, dim=1).numpy()
			h1_ = torch.from_numpy(dct(h1, type=2, axis=1))
			h1_ = 0.5 * (h1_ ** 2).log()
			h2 = []
			for j in range(0, 900):
				h2.append((h1_[:, j:(j + 100)] * triangular).sum(dim=-1, keepdim=True))
			h2 = torch.cat(h2, dim=1)
			for j in range(0, 20):
				fig = pyplot.figure(i // 1000 + 1)
				pyplot.plot(h2[j])
				s_name = "./encephalograms_log_spectrae/"
				s_name = s_name + format(j + 1)
				s_name = s_name + "/pic_" + format(p[j])
				s_name = s_name + "_" + format(q[j])
				s_name = s_name + "_" + format(m[j])
				s_name = s_name + "_" + format(n[j])
				s_name = s_name + "_" + format(i // 1000 + 1)
				s_name = s_name + ".jpg"
				pyplot.savefig(s_name)
				pyplot.close(fig)
				fig = pyplot.figure(i // 1000 + 1, figsize=(20, 6), dpi=120)
				pyplot.plot(h1[j])
				s_name = "./encephalograms/"
				s_name = s_name + format(j + 1)
				s_name = s_name + "/pic_" + format(p[j])
				s_name = s_name + "_" + format(q[j])
				s_name = s_name + "_" + format(m[j])
				s_name = s_name + "_" + format(n[j])
				s_name = s_name + "_" + format(i // 1000 + 1)
				s_name = s_name + ".jpg"
				pyplot.savefig(s_name)
				pyplot.close(fig)
			h1 = []

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

Полный код я выложил здесь с образцами ЭЭГшек и анимации:

https://disk.yandex.ru/d/SOmVw9MYM9Cv8Q

Инструкция простая: тренируем VAE в train.py, далее запускаем анимацию в test.py, а собираем её в source_maker.py, порядок именно такой.

Вот, кстати и пример ЭЭГ со спектром:

Источник: https://habr.com/ru/post/653237/


Интересные статьи

Интересные статьи

Всем привет, меня зовут Алена Коваленко, я Java-разработчица одной из команд направления Warehouse Management System (WMS) компании Lamoda. Наша команда занимается автоматизацией складской системы и р...
Любая компания рано или поздно сталкивается с вопросом обработки большого объема входящей документации. В первую очередь это может быть, например, бухгалтерия со своими первичными документами: счетами...
Мы в команде Ptah мы решили пойти чуть дальше привычных SPA и попробовали использовать Vue для конструктора лендингов. И теперь хотим поделиться частью нашего опыта. Эта статья, прежде всего, ...
Компании переполнили рынок товаров и услуг предложениями. Разнообразие наблюдается не только в офлайне, но и в интернете. Достаточно вбить в поисковик любой запрос, чтобы получить подтверждение насыще...
В современном вебе, время загрузки страницы сайта — одна из важнейших метрик. Даже миллисекунды могу оказывать огромное влияние на Вашу прибыль и медленная загрузка страницы может легко навредить...