Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру 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, порядок именно такой.
Вот, кстати и пример ЭЭГ со спектром: