Приветствую всех вОблако Tencent + сообщество, получить больше крупной технической практики Tencent по галантерее~
Эта статья написанаРедактор информации об искусственном интеллектеОпубликован вКолонка «Облако + сообщество»
Длинное текстовое предупреждение:В этой статье мы будем обучать нейронную сеть, которая при обучении полностью зашифрована (обучение на незашифрованных данных). Это придаст нейронным сетям два полезных свойства: во-первых, интеллект нейронных сетей может быть лучше защищен от кражи другими, исключая возможность кражи ценного ИИ, обученного в небезопасной среде, из-за риска кражи другими интеллектами; во-вторых, сеть можетДелайте только зашифрованные прогнозы(Это означает, что без ключа внешний мир не может понять и принять предсказание, поэтому сеть не имеет никакого влияния на внешний мир). Это создает очень ценную неравную силу между пользователем и сверхразумом. Только представьте, если ИИ обрабатывается гомоморфным шифрованием, то с точки зрения ИИ,Весь внешний мир также гомоморфно зашифрован.. Люди могут контролировать ключ и решать, разблокировать ли сам ИИ (опубликованный во внешнем мире) или просто расшифровать решения, которые принимает ИИ (кажется более безопасным).
Примечание:Если вы заинтересованы в обучении зашифрованных нейронных сетей, вы можете обратиться кБиблиотека OpenMined PySyft
супер умный
Многие люди опасаются, что сверхразум однажды решит причинить вред людям. Стивен Хокинг ранее призывал кНовый Свет "Правительство"управлять возможностями, которые мы даем искусственному интеллекту, чтобы он не уничтожил человечество. Это смелое заявление, и я думаю, что оно отражает общую заботу научного сообщества и всего мира. В этой статье я продемонстрирую потенциальное техническое решение этой проблемы с помощью некоторого игрушечного кода.
Наша цель проста. Мы словно изобретаем технологию, которая может сделать ИИ чрезвычайно умным (достаточно умным, чтобы вылечить рак и решить глобальный голод), но только в том случае, если такой интеллект находится под контролем человека, то есть его применение ограничено. Неограниченное обучение — это хорошо, но неограниченное применение знаний потенциально опасно.
Чтобы проиллюстрировать это, я начну с краткого обзора двух захватывающих областей исследований: глубокого обучения и гомоморфного шифрования.
Часть 1: Что такое глубокое обучение?
Глубокое обучение можно понимать как набор автоматизированных инструментов для искусственного интеллекта, в основном с помощью нейронных сетей. Как одна из областей компьютерных наук, глубокое обучение превзошло предыдущие технологии по качеству выполнения многих интеллектуальных задач, что привело к процветанию технологии ИИ. В этом контексте после победы над чемпионом мира по гоAlphaGo от DeepMindГлубокое обучение также играет важную роль.
проблема:Как обучаются нейронные сети?
Нейронные сети делают прогнозы на основе входных данных. Он делает это эффективно за счет постоянных экспериментов. Процесс начинается с предсказания (сначала в основном случайного), а затем использует полученный «сигнал ошибки», чтобы определить, является ли его предсказание высоким или низким (обычно результатом предсказания является значение вероятности). После многих попыток в сети появилась возможность идентификации. Для получения более подробной информации о том, как работают нейронные сети, см.A Neural Network in 11 Lines of Python.
Дело здесь в упомянутом выше сигнале ошибки. Имейте это в виду, если сеть не может учиться, если она не знает, насколько хороши ее прогнозы.
Часть 2: Что такое гомоморфное шифрование?
Как следует из названия,Гомоморфное шифрованиеявляется формой шифрования. В случае асимметрии «открытый ключ» можно использовать для преобразования открытого текста в искаженные символы. Ключевым моментом является то, что вы можете использовать соответствующий «закрытый ключ» для повторного декодирования зашифрованного текста в исходный открытый текст. Но вы можете декодировать запутанный открытый текст (теоретически), только если у вас есть «закрытый ключ».
Гомоморфное шифрование — это всего лишь особый вид шифрования. Он позволяет некоторым пользователям изменять зашифрованную информацию без разрешения на чтение исходной информации. Например, зашифрованную цифровую информацию можно размножать и добавлять без декодирования. Простой пример приведен ниже:
Сейчас схем гомоморфного шифрования становится все больше, и каждая схема имеет разные характеристики. Тем не менее, это все еще развивающаяся область, и предстоит решить еще много ключевых вопросов, и мы вернемся к этому вопросу позже.
Теперь давайте продолжим объяснение вышеприведенного содержания.Из приведенного выше рисунка видно, что мы можем выполнять операции умножения и сложения с гомоморфно зашифрованными данными. Кроме того, поскольку открытый ключ может выполнять одностороннее шифрование, что позволяет нам выполнять соответствующие операции с зашифрованными номерами и незашифрованными номерами (одностороннее шифрование незашифрованных данных), как и 2*Cypher на приведенном выше рисунке. (Некоторым алгоритмам шифрования даже этого делать не нужно, мы поговорим об этом позже)
Часть 3: Можем ли мы использовать их комбинацию?
Наиболее распространенная комбинация глубокого обучения и гомоморфного шифрования в основном отражает конфиденциальность данных. Получается, что когда данные гомоморфно зашифрованы, информация в них не может быть прочитана, но вы все равно можете сохранить в данных большую часть интересующей вас статистической структуры. Это позволяет обучать модели на зашифрованных данных (CryptoNets). Кроме того, начинающие хедж-фондыNumer.aiЗашифрованные дорогие закрытые данные для любого, кто может обучить модель машинного обучения для прогнозирования фондового рынка. Как правило, они не могут, потому что это представляет собой утечку ценных/частных данных (и обычное шифрование сделало бы обучение модели невозможным).
Однако в этой статье делается обратный процесс, т. е. шифрование нейронной сети и ее обучение на декодированных данных.
Общая сложность большой нейронной сети ошеломляет, но их разбивка — это просто повторение некоторых простых операций. Фактически, для создания многих продвинутых нейронных сетей обычно требуются только следующие операции:
- добавление
- умножение
- разделение
- вычитание
- Sigmoid
- tanh
- Экспоненциальная функция
В таком случае, можем ли мы технически зашифровать саму гомоморфную нейронную сеть? Оказывается, это можно сделать с некоторыми приближениями.
- Дополнение - из коробки
- Умножение - из коробки
- Дивизион - из коробки? - Умножить (1 / множимое)
- Вычитание - из коробки? - просто добавьте отрицательное число
- Sigmoid- Хм... может быть, немного сложно
- Tanh- Хм... может быть, немного сложно
- показатель- Хм... может быть, немного сложно
Похоже, мы можем довольно легко выполнять деление и вычитание, но некоторые сложные функции гораздо сложнее, чем сложение и умножение. Чтобы гомоморфно зашифровать глубокие нейронные сети, нам нужны некоторые «секретные рецепты».
Часть 4: Расширение серии Тейлора
Возможно, вы помните этот раздел из средней школы или колледжа:Серия ТейлораПозволяет нам вычислять нелинейные функции, используя комбинацию операций сложения, вычитания, умножения и деления с бесконечными членами. Этот метод отлично решил нашу проблему! (За исключением необходимости бесконечных предметов). К счастью, если вы можете вычислить только несколько первых членов разложения Тейлора, вы также можете получить приблизительную функцию. Следующее шоу дает серию Тейлора некоторых часто используемых функций (источник):
Среди них экспоненциальная функция! Вы можете видеть, что расширение — это просто некоторые операции сложения, вычитания, умножения и деления, которые мы можем делать. Точно так же мы можем реализовать разложение Тейлора нужной нам сигмовидной функции в Python, как показано на рисунке ниже (его разложение можно найти вWolfram Alphaчек). Мы можем только взять некоторые из предыдущих членов, чтобы увидеть разрыв между приблизительными результатами и фактическими результатами.
import numpy as np
def sigmoid_exact(x):
return 1 / (1 + np.exp(-x))
# using taylor series
def sigmoid_approximation(x):
return (1 / 2) + (x / 4) - (x**3 / 48) + (x**5 / 480)
for lil_number in [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]:
print("\nInput:" + str(lil_number))
print("Exact Sigmoid:" + str(sigmoid_exact(lil_number)))
print("Approx Sigmoid:" + str(sigmoid_approximation(lil_number)))
Input:0.1
Exact Sigmoid:0.524979187479
Approx Sigmoid:0.5249791875
Input:0.2
Exact Sigmoid:0.549833997312
Approx Sigmoid:0.549834
Input:0.3
Exact Sigmoid:0.574442516812
Approx Sigmoid:0.5744425625
Input:0.4
Exact Sigmoid:0.598687660112
Approx Sigmoid:0.598688
Input:0.5
Exact Sigmoid:0.622459331202
Approx Sigmoid:0.6224609375
Input:0.6
Exact Sigmoid:0.645656306226
Approx Sigmoid:0.645662
Input:0.7
Exact Sigmoid:0.668187772168
Approx Sigmoid:0.6682043125
Input:0.8
Exact Sigmoid:0.689974481128
Approx Sigmoid:0.690016
Input:0.9
Exact Sigmoid:0.710949502625
Approx Sigmoid:0.7110426875
Input:1.0
Exact Sigmoid:0.73105857863
Approx Sigmoid:0.73125
Из результатов видно, что результат расчета, когда мы берем только первые четыре члена, очень близок к результату расчета бесконечного члена. После решения задачи о сложных функциях выберем гомоморфный алгоритм шифрования.
Часть 5: Выбор алгоритма шифрования
Гомоморфное шифрование — относительно новая область, и ее вехой стало изобретение Крейга Джентри в 2009 году.Первый полностью гомоморфный алгоритм шифрования. Это открытие послужило опорой для более поздних исследователей. Большая часть захватывающих исследований в этой области связана с компьютерами, полными по Тьюрингу, которые реализуют гомоморфное шифрование. Соответственно, людям необходимо найти эффективную и безопасную схему гомоморфного алгоритма шифрования, которая может завершить соответствующую операцию логического элемента в соответствии с любым вводом вычислений. Существует широко распространенная надежда на то, что работу можно безопасно перенести в облако, не беспокоясь о том, что отправка данных может быть отслежена кем-то, кроме отправителя. Это действительно крутая идея и большой прогресс.
Но есть и недостаток, заключающийся в том, что большинство полностью гомоморфных алгоритмов шифрования обычно очень медленны (непрактичны) на обычных компьютерах. Это приводит к другому интересному направлению исследований, которое заключается в выполнении только частичных гомоморфных операций шифрования для уменьшения объема вычислений. Эта схема снижает гибкость, но повышает скорость работы и является распространенным компромиссным методом в вычислениях.
Мы ищем алгоритмы шифрования с этой точки. Теоретически нам нужна гомоморфная схема шифрования в операциях с плавающей запятой (но мы используем целые числа в качестве примера в этой статье), почему не бинарная? Двоичное шифрование возможно, но оно требует не только гибкости (ухудшения производительности) полностью гомоморфной схемы шифрования, но также требует, чтобы мы управляли двоичным представлением и логическими операциями в математических операциях. Напротив, менее мощный алгоритм HE, адаптированный для чисел с плавающей запятой, более подходит.
Хотя мы определили это ограничение, есть еще много алгоритмов на выбор. Вот некоторые популярные алгоритмы с нашими любимыми свойствами:
- Эффективное гомоморфное шифрование на основе целочисленного вектора и его применение
- Yet Another Somewhat Homomorphic Encryption (YASHE)
- Somewhat Practical Fully Homomorphic Encryption (FV)
- Fully Homomorphic Encryption without Bootstrapping
Оптимальным решением среди вышеперечисленных решений должно быть YASHE или FV. YASHE используется в популярных алгоритмах CryptoNets (зашифрованных сетей) и очень хорошо поддерживает операции с плавающей запятой. Но этот метод сложен, и чтобы сделать эту статью проще и интереснее, мы выбираем несколько худшую схему (эффективное гомоморфное шифрование на основе целочисленных векторов,менее безопасный). Тем не менее, вы должны отметить, что, несмотря на появление новых алгоритмов гомоморфного шифрования, когда вы читаете эту статью, реализации гомоморфного шифрования сложения целых чисел и чисел с плавающей запятой и умножения в этой статье являются общими. Я надеюсь, что с помощью этой статьи вы сможете улучшить свое понимание применения алгоритмов гомоморфного шифрования, чтобы разработать больше алгоритмов гомоморфного шифрования для оптимизации глубокого обучения.
этот алгоритм шифрованияРабота Ю, Лай и ПэйлорТам же есть раздел, посвященный этому, и соответствующийКод, основной код находится вvhe.cpp
среди. Ниже мы будем использовать Python для реализации соответствующего интерфейса и соответствующего объяснения. Вы также можете выбрать реализацию более высокого уровня или перенести ее на другие языки или сценарии, потому что эта реализация является универсальной (общие имена функций, имена переменных и т. д.).
Часть 6: Гомоморфное шифрование в Python
Давайте сначала введем терминологию в гомоморфном шифровании:
- Простой текст:Незашифрованные данные. Это также называется «сообщением». В нашем случае это будет набор числовых значений, представляющих нашу нейронную сеть.
- Зашифрованный текст:зашифрованные данные. Математика, которую мы делаем с зашифрованным текстом, изменяет базовый открытый текст, как упоминалось в исходной диаграмме.
- открытый ключ:Последовательность псевдослучайных чисел, которую любой может использовать для шифрования данных. Открытый ключ можно распространять, потому что человек, который его получает, может использовать его только для шифрования данных (теоретически).
- Частный/Ключ:Последовательность псевдослучайных чисел, позволяющая расшифровывать данные, зашифрованные с помощью открытого ключа. Закрытые ключи не должны передаваться другим, иначе ваши зашифрованные данные будут расшифрованы другими.
Выше перечислены компоненты, которые можно перемещать при использовании. В различных алгоритмах гомоморфного шифрования будут некоторые стандартные переменные.В эту статью включены следующие стандартные переменные:
- С:Матрица, представляющая ключ/закрытый ключ, необходимый для расшифровки сообщения.
- М:открытый ключ. Вы будете использовать его для шифрования вещей и выполнения математических операций. Хотя некоторые алгоритмы не требуют открытых ключей для всех математических операций, открытые ключи по-прежнему широко используются.
- от:Вектор зашифрованных данных, «зашифрованный текст».
- маленький:Соответствует вашему сообщению или вашему «открытому тексту». В некоторых статьях вместо этого используется переменная «m».
- w :Для ввода информации намxПостоянное значение для перераспределения весов. (сделайте его всегда больше или меньше). Мы используем эту переменную, чтобы настроить отношение сигнал/шум. Увеличение сигнала делает его менее восприимчивым к шуму в любой заданной операции. Однако, если данные слишком велики, это увеличивает вероятность того, что мы полностью испортим данные. Это скомпрометированное значение.
- E/e: Обычно относится к случайному шуму. В некоторых случаях перед шифрованием данные, зашифрованные с открытым ключом, смешиваются с шумом. Смешанный шум затрудняет расшифровку. Но это также позволяет одному и тому же открытому тексту генерировать разные зашифрованные тексты при использовании одного и того же открытого ключа, что увеличивает сложность взлома зашифрованного текста. Обратите внимание, что в зависимости от алгоритма и реализацииE/eМожет быть вектором или матрицей. В других случаях это может также относиться к шуму, накопленному в результате операции. Мы обсудим это позже.
Как и во многих математических работах, прописные буквы соответствуют матрицам, строчные буквы соответствуют векторам, а строчные курсивные буквы соответствуют скалярам. Гомоморфное шифрование включает четыре операции, которые нас интересуют: генерация пар открытого и закрытого ключей, одностороннее шифрование, дешифрование и математические операции. Начнем с расшифровки.
Приведенная выше формула описывает закрытый ключSи наш "открытый текст"xобщие отношения между. Приведенная ниже формула говорит нам, как расшифровать данные с помощью закрытого ключа. Обратите внимание на следующую формулу,eКуда это делось? На самом деле, в принципе, цель введения шума в гомоморфное шифрование состоит в том, чтобы затруднить людям расшифровку наших данных без закрытого ключа, но если у вас есть ключ, шум настолько мал, что равен или меньше чем ошибка округления. Специальные скобки в приведенной ниже формуле указывают на то, что результат расчета округляется до ближайшего целого числа. Другие алгоритмы шифрования могут выбирать другие методы округления. Оператор по модулю еще более распространен, и процесс шифрования на самом деле заключается в созданииc. еслиSявляется случайной матрицей,cрасшифровать будет сложно. Короче говоря, в асимметричном случае для создания ключа шифрования требуется только найти обратную сторону закрытого ключа. Мы используем код Python, чтобы продемонстрировать следующее:
import numpy as np
def generate_key(w,m,n):
S = (np.random.rand(m,n) * w / (2 ** 16)) # proving max(S) < w
return S
def encrypt(x,S,m,n,w):
assert len(x) == len(S)
e = (np.random.rand(m)) # proving max(e) < w / 2
c = np.linalg.inv(S).dot((w * x) + e)
return c
def decrypt(c,S,w):
return (S.dot(c) / w).astype('int')
def get_c_star(c,m,l):
c_star = np.zeros(l * m,dtype='int')
for i in range(m):
b = np.array(list(np.binary_repr(np.abs(c[i]))),dtype='int')
if(c[i] < 0):
b *= -1
c_star[(i * l) + (l-len(b)): (i+1) * l] += b
return c_star
def get_S_star(S,m,n,l):
S_star = list()
for i in range(l):
S_star.append(S*2**(l-i-1))
S_star = np.array(S_star).transpose(1,2,0).reshape(m,n*l)
return S_star
x = np.array([0,1,2,5])
m = len(x)
n = m
w = 16
S = generate_key(w,m,n)
Я сделал следующее после запуска этого кода в блокноте iPython (с соответствующим выводом).
Ключ является результатом следующих двух операций.Вы можете видеть, что основные арифметические операции, которые мы выполняем с открытым текстом, изменяют открытый текст, представленный нижним слоем зашифрованного текста. Довольно элегантная операция, не правда ли?
Часть 7: Оптимизация процесса шифрования
Вводить:Давайте еще раз посмотрим на формулу расшифровки: если ключ S является единичной матрицей, то зашифрованный текст c — это просто перевзвешенный открытый текст, смешанный с определенным количеством шума, который можно найти, пока есть определенное количество выборок. Если вы не понимаете этот текст, вам нужно поискать знания, связанные с «матрицей идентичности», и вернуться к чтению, иначе последующее содержание будет очень трудно понять.
В приведенном выше примере показано, как происходит шифрование. Изобретатель этой технологии не определил пару независимых «открытый ключ» и «закрытый ключ» одновременно, а предложил технологию «преобразования ключей», с помощью которой закрытый ключ S может быть преобразован в S'. В частности, этот метод преобразования ключа генерирует матрицы M, M, которые могут преобразовывать незашифрованные данные (матрица идентичности в качестве ключа) в зашифрованные данные (случайно сгенерированный, трудно угадываемый ключ). Это наш открытый ключ!
Вышеприведенный абзац содержит много информации, давайте еще раз разберемся:
что тут происходит...
- Согласно двум приведенным выше формулам, если ключ представляет собой единичную матрицу, информация не шифруется.
- Согласно двум приведенным выше формулам, если ключ представляет собой случайную матрицу, сгенерированная информация шифруется.
- Мы можем создать матрицу M, которая изменяет ключ из одной формы в другую.
- Когда матрица M преобразуется из матрицы идентичности в ключ случайной матрицы, она завершает одностороннее шифрование информации путем расширения.
- Поскольку M действует как «одностороннее шифрование», мы называем его «открытым ключом» и распространяем его как открытый ключ, поскольку он не расшифровывает код.
Мы не будем углубляться в этот момент, давайте посмотрим, как этот процесс выполняется в Python:
import numpy as np
def generate_key(w,m,n):
S = (np.random.rand(m,n) * w / (2 ** 16)) # proving max(S) < w
return S
def encrypt(x,S,m,n,w):
assert len(x) == len(S)
e = (np.random.rand(m)) # proving max(e) < w / 2
c = np.linalg.inv(S).dot((w * x) + e)
return c
def decrypt(c,S,w):
return (S.dot(c) / w).astype('int')
def get_c_star(c,m,l):
c_star = np.zeros(l * m,dtype='int')
for i in range(m):
b = np.array(list(np.binary_repr(np.abs(c[i]))),dtype='int')
if(c[i] < 0):
b *= -1
c_star[(i * l) + (l-len(b)): (i+1) * l] += b
return c_star
def switch_key(c,S,m,n,T):
l = int(np.ceil(np.log2(np.max(np.abs(c)))))
c_star = get_c_star(c,m,l)
S_star = get_S_star(S,m,n,l)
n_prime = n + 1
S_prime = np.concatenate((np.eye(m),T.T),0).T
A = (np.random.rand(n_prime - m, n*l) * 10).astype('int')
E = (1 * np.random.rand(S_star.shape[0],S_star.shape[1])).astype('int')
M = np.concatenate(((S_star - T.dot(A) + E),A),0)
c_prime = M.dot(c_star)
return c_prime,S_prime
def get_S_star(S,m,n,l):
S_star = list()
for i in range(l):
S_star.append(S*2**(l-i-1))
S_star = np.array(S_star).transpose(1,2,0).reshape(m,n*l)
return S_star
def get_T(n):
n_prime = n + 1
T = (10 * np.random.rand(n,n_prime - n)).astype('int')
return T
def encrypt_via_switch(x,w,m,n,T):
c,S = switch_key(x*w,np.eye(m),m,n,T)
return c,S
x = np.array([0,1,2,5])
m = len(x)
n = m
w = 16
S = generate_key(w,m,n)
Этот метод в основном состоит в том, чтобы в большинстве случаев ключ S был единичной матрицей, а затем комбинировал его со случайным вектором T. Хотя у T есть вся необходимая информация для ключа, нам также нужно создать матрицу с теми же строками и столбцами, что и у S, чтобы выполнить эту работу.
Часть 8: Создание нейронной сети XOR
Теперь, когда мы знаем, как шифровать и расшифровывать информацию (и реализовывать базовые операции сложения и умножения), пришло время расширить оставшиеся операции для реализации простой нейронной сети XOR. Хотя технически нейронная сеть — это всего лишь набор простых операций, для удобства нам нужно инкапсулировать некоторые часто используемые комбинаторные операции в виде функций. Ниже я описываю операции, которые нам нужны, и некоторые передовые методы, которые мы используем для их достижения, а затем перечисляю соответствующие реализации кода. Для получения более подробной информации см.Исследования Анхеля Ю, Вай Лок Лая, Джеймса Пайора.
- число с плавающей запятой:Мы сделаем это, просто увеличив число с плавающей запятой до целого числа. Таким образом, даже если ввод представляет собой число с плавающей запятой, мы также можем обработать его как целое число. Допустим, мы выбираем увеличение 1000. 0.2 до увеличения0,5 = 0,1. Увеличил до 200500 = 100000. Обратите внимание, что мы должны масштабироваться в 2 раза после выполнения 2 умножений, то есть 100000 / (1000 * 1000) = 0,1. Это трюк, к которому нужно привыкнуть. Поскольку схемы гомоморфного шифрования в конечном итоге округляются до ближайшего целого числа, масштабирование также может помочь вам контролировать точность вашей сети.
- Умножение векторной матрицы:Это основная функция, которую мы хотим выполнить. Фактически процесс преобразования одного ключа в М-матрицу другого ключа на самом деле представляет собой линейное преобразование.
- Скалярное произведение:В правильном контексте приведенное выше линейное преобразование также можно записать в виде скалярного произведения.
- Сигмоид:Поскольку мы можем выполнять векторно-матричное умножение, мы можем оценить подходящие произвольные многочлены с достаточным умножением. Поскольку мы знаем многочлен ряда Тейлора сигмоиды, мы можем подогнать приблизительную сигмовидную функцию!
- Умножение элементов матрицы:Эта операция крайне неэффективна. Мы должны сделать умножение векторной матрицы или серию скалярных произведений.
- Внешний продукт:Мы можем сделать это путем отсечения и внешнего произведения матриц.
Прежде всего утверждается, что могут быть и другие более эффективные способы достижения вышеуказанной операции, но чтобы не сломать текущую схему гомоморфного шифрования, мне пришлось использовать функцию, представленную в статье (реализующую доступные расширения сигмоида) . Теперь давайте посмотрим, как это сделать в Python.
def sigmoid(layer_2_c):
out_rows = list()
for position in range(len(layer_2_c)-1):
M_position = M_onehot[len(layer_2_c)-2][0]
layer_2_index_c = innerProd(layer_2_c,v_onehot[len(layer_2_c)-2][position],M_position,l) / scaling_factor
x = layer_2_index_c
x2 = innerProd(x,x,M_position,l) / scaling_factor
x3 = innerProd(x,x2,M_position,l) / scaling_factor
x5 = innerProd(x3,x2,M_position,l) / scaling_factor
x7 = innerProd(x5,x2,M_position,l) / scaling_factor
xs = copy.deepcopy(v_onehot[5][0])
xs[1] = x[0]
xs[2] = x2[0]
xs[3] = x3[0]
xs[4] = x5[0]
xs[5] = x7[0]
out = mat_mul_forward(xs,H_sigmoid[0:1],scaling_factor)
out_rows.append(out)
return transpose(out_rows)[0]
def load_linear_transformation(syn0_text,scaling_factor = 1000):
syn0_text *= scaling_factor
return linearTransformClient(syn0_text.T,getSecretKey(T_keys[len(syn0_text)-1]),T_keys[len(syn0_text)-1],l)
def outer_product(x,y):
flip = False
if(len(x) < len(y)):
flip = True
tmp = x
x = y
y = tmp
y_matrix = list()
for i in range(len(x)-1):
y_matrix.append(y)
y_matrix_transpose = transpose(y_matrix)
outer_result = list()
for i in range(len(x)-1):
outer_result.append(mat_mul_forward(x * onehot[len(x)-1][i],y_matrix_transpose,scaling_factor))
if(flip):
return transpose(outer_result)
return outer_result
def mat_mul_forward(layer_1,syn1,scaling_factor):
input_dim = len(layer_1)
output_dim = len(syn1)
buff = np.zeros(max(output_dim+1,input_dim+1))
buff[0:len(layer_1)] = layer_1
layer_1_c = buff
syn1_c = list()
for i in range(len(syn1)):
buff = np.zeros(max(output_dim+1,input_dim+1))
buff[0:len(syn1[i])] = syn1[i]
syn1_c.append(buff)
layer_2 = innerProd(syn1_c[0],layer_1_c,M_onehot[len(layer_1_c) - 2][0],l) / float(scaling_factor)
for i in range(len(syn1)-1):
layer_2 += innerProd(syn1_c[i+1],layer_1_c,M_onehot[len(layer_1_c) - 2][i+1],l) / float(scaling_factor)
return layer_2[0:output_dim+1]
def elementwise_vector_mult(x,y,scaling_factor):
y =[y]
one_minus_layer_1 = transpose(y)
outer_result = list()
for i in range(len(x)-1):
outer_result.append(mat_mul_forward(x * onehot[len(x)-1][i],y,scaling_factor))
return transpose(outer_result)[0]
Еще одна вещь, которую я не упомянул, просто для экономии времени я предварительно вычислил несколько ключей, векторов, матриц и сохранил их. Он включает в себя матрицы, все матричные элементы которых равны 1, векторы прямого кодирования неопределенной длины и т. д. Это обеспечивает большое удобство для упомянутой выше операции обрезки, а также для других простых операций. Например, производная сигмовидной функции равна сигмовидной (x) * (1 - сигмовидной (x)). Поэтому предварительное вычисление этих значений полезно. Соответствующий код приведен ниже:
# HAPPENS ON SECURE SERVER
l = 100
w = 2 ** 25
aBound = 10
tBound = 10
eBound = 10
max_dim = 10
scaling_factor = 1000
# keys
T_keys = list()
for i in range(max_dim):
T_keys.append(np.random.rand(i+1,1))
# 单向加密转换
M_keys = list()
for i in range(max_dim):
M_keys.append(innerProdClient(T_keys[i],l))
M_onehot = list()
for h in range(max_dim):
i = h+1
buffered_eyes = list()
for row in np.eye(i+1):
buffer = np.ones(i+1)
buffer[0:i+1] = row
buffered_eyes.append((M_keys[i-1].T * buffer).T)
M_onehot.append(buffered_eyes)
c_ones = list()
for i in range(max_dim):
c_ones.append(encrypt(T_keys[i],np.ones(i+1), w, l).astype('int'))
v_onehot = list()
onehot = list()
for i in range(max_dim):
eyes = list()
eyes_txt = list()
for eye in np.eye(i+1):
eyes_txt.append(eye)
eyes.append(one_way_encrypt_vector(eye,scaling_factor))
v_onehot.append(eyes)
onehot.append(eyes_txt)
H_sigmoid_txt = np.zeros((5,5))
H_sigmoid_txt[0][0] = 0.5
H_sigmoid_txt[0][1] = 0.25
H_sigmoid_txt[0][2] = -1/48.0
H_sigmoid_txt[0][3] = 1/480.0
H_sigmoid_txt[0][4] = -17/80640.0
H_sigmoid = list()
for row in H_sigmoid_txt:
H_sigmoid.append(one_way_encrypt_vector(row))
Если вы внимательно посмотрите, то увидите, что матрица H_sigmoid — это именно то, что нам нужно для оценки сигмовидной функции. :) Наконец, мы используем следующий код для обучения нашей нейронной сети. Если вам непонятна концепция нейронной сети, вы можете выполнить поиск самостоятельно или проверить сообщение в блоге автора.A Neural Network in 11 Lines of Python, нейронная сеть XOR здесь в основном относится к этой статье, но заменена в соответствии с приведенной выше функцией операции шифрования.
np.random.seed(1234)
input_dataset = [[],[0],[1],[0,1]]
output_dataset = [[0],[1],[1],[0]]
input_dim = 3
hidden_dim = 4
output_dim = 1
alpha = 0.015
# 利用公钥实现单向加密
y = list()
for i in range(4):
y.append(one_way_encrypt_vector(output_dataset[i],scaling_factor))
# 生成权重值
syn0_t = (np.random.randn(input_dim,hidden_dim) * 0.2) - 0.1
syn1_t = (np.random.randn(output_dim,hidden_dim) * 0.2) - 0.1
# 单向加密权重值
syn1 = list()
for row in syn1_t:
syn1.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
syn0 = list()
for row in syn0_t:
syn0.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
# 开始训练
for iter in range(1000):
decrypted_error = 0
encrypted_error = 0
for row_i in range(4):
if(row_i == 0):
layer_1 = sigmoid(syn0[0])
elif(row_i == 1):
layer_1 = sigmoid((syn0[0] + syn0[1])/2.0)
elif(row_i == 2):
layer_1 = sigmoid((syn0[0] + syn0[2])/2.0)
else:
layer_1 = sigmoid((syn0[0] + syn0[1] + syn0[2])/3.0)
layer_2 = (innerProd(syn1[0],layer_1,M_onehot[len(layer_1) - 2][0],l) / float(scaling_factor))[0:2]
layer_2_delta = add_vectors(layer_2,-y[row_i])
syn1_trans = transpose(syn1)
one_minus_layer_1 = [(scaling_factor * c_ones[len(layer_1) - 2]) - layer_1]
sigmoid_delta = elementwise_vector_mult(layer_1,one_minus_layer_1[0],scaling_factor)
layer_1_delta_nosig = mat_mul_forward(layer_2_delta,syn1_trans,1).astype('int64')
layer_1_delta = elementwise_vector_mult(layer_1_delta_nosig,sigmoid_delta,scaling_factor) * alpha
syn1_delta = np.array(outer_product(layer_2_delta,layer_1)).astype('int64')
syn1[0] -= np.array(syn1_delta[0]* alpha).astype('int64')
syn0[0] -= (layer_1_delta).astype('int64')
if(row_i == 1):
syn0[1] -= (layer_1_delta).astype('int64')
elif(row_i == 2):
syn0[2] -= (layer_1_delta).astype('int64')
elif(row_i == 3):
syn0[1] -= (layer_1_delta).astype('int64')
syn0[2] -= (layer_1_delta).astype('int64')
# 为了监视训练情况,下面我将解码损失值
# 如果当前环境并不安全,我会将加密后的损失值发送至安全的地方解码
encrypted_error += int(np.sum(np.abs(layer_2_delta)) / scaling_factor)
decrypted_error += np.sum(np.abs(s_decrypt(layer_2_delta).astype('float')/scaling_factor))
sys.stdout.write("\r Iter:" + str(iter) + " Encrypted Loss:" + str(encrypted_error) + " Decrypted Loss:" + str(decrypted_error) + " Alpha:" + str(alpha))
# 使输出更美观
if(iter % 10 == 0):
print()
# 加密误差达到指定值后停止训练
if(encrypted_error < 25000000):
break
print("\nFinal Prediction:")
for row_i in range(4):
if(row_i == 0):
layer_1 = sigmoid(syn0[0])
elif(row_i == 1):
layer_1 = sigmoid((syn0[0] + syn0[1])/2.0)
elif(row_i == 2):
layer_1 = sigmoid((syn0[0] + syn0[2])/2.0)
else:
layer_1 = sigmoid((syn0[0] + syn0[1] + syn0[2])/3.0)
layer_2 = (innerProd(syn1[0],layer_1,M_onehot[len(layer_1) - 2][0],l) / float(scaling_factor))[0:2]
print("True Pred:" + str(output_dataset[row_i]) + " Encrypted Prediction:" + str(layer_2) + " Decrypted Prediction:" + str(s_decrypt(layer_2) / scaling_factor))
Iter:0 Encrypted Loss:84890656 Decrypted Loss:2.529 Alpha:0.015
Iter:10 Encrypted Loss:69494197 Decrypted Loss:2.071 Alpha:0.015
Iter:20 Encrypted Loss:64017850 Decrypted Loss:1.907 Alpha:0.015
Iter:30 Encrypted Loss:62367015 Decrypted Loss:1.858 Alpha:0.015
Iter:40 Encrypted Loss:61874493 Decrypted Loss:1.843 Alpha:0.015
Iter:50 Encrypted Loss:61399244 Decrypted Loss:1.829 Alpha:0.015
Iter:60 Encrypted Loss:60788581 Decrypted Loss:1.811 Alpha:0.015
Iter:70 Encrypted Loss:60327357 Decrypted Loss:1.797 Alpha:0.015
Iter:80 Encrypted Loss:59939426 Decrypted Loss:1.786 Alpha:0.015
Iter:90 Encrypted Loss:59628769 Decrypted Loss:1.778 Alpha:0.015
Iter:100 Encrypted Loss:59373621 Decrypted Loss:1.769 Alpha:0.015
Iter:110 Encrypted Loss:59148014 Decrypted Loss:1.763 Alpha:0.015
Iter:120 Encrypted Loss:58934571 Decrypted Loss:1.757 Alpha:0.015
Iter:130 Encrypted Loss:58724873 Decrypted Loss:1.75 Alpha:0.0155
Iter:140 Encrypted Loss:58516008 Decrypted Loss:1.744 Alpha:0.015
Iter:150 Encrypted Loss:58307663 Decrypted Loss:1.739 Alpha:0.015
Iter:160 Encrypted Loss:58102049 Decrypted Loss:1.732 Alpha:0.015
Iter:170 Encrypted Loss:57863091 Decrypted Loss:1.725 Alpha:0.015
Iter:180 Encrypted Loss:55470158 Decrypted Loss:1.653 Alpha:0.015
Iter:190 Encrypted Loss:54650383 Decrypted Loss:1.629 Alpha:0.015
Iter:200 Encrypted Loss:53838756 Decrypted Loss:1.605 Alpha:0.015
Iter:210 Encrypted Loss:51684722 Decrypted Loss:1.541 Alpha:0.015
Iter:220 Encrypted Loss:54408709 Decrypted Loss:1.621 Alpha:0.015
Iter:230 Encrypted Loss:54946198 Decrypted Loss:1.638 Alpha:0.015
Iter:240 Encrypted Loss:54668472 Decrypted Loss:1.63 Alpha:0.0155
Iter:250 Encrypted Loss:55444008 Decrypted Loss:1.653 Alpha:0.015
Iter:260 Encrypted Loss:54094286 Decrypted Loss:1.612 Alpha:0.015
Iter:270 Encrypted Loss:51251831 Decrypted Loss:1.528 Alpha:0.015
Iter:276 Encrypted Loss:24543890 Decrypted Loss:0.732 Alpha:0.015
Final Prediction:
True Pred:[0] Encrypted Prediction:[-3761423723.0718255 0.0] Decrypted Prediction:[-0.112]
True Pred:[1] Encrypted Prediction:[24204806753.166267 0.0] Decrypted Prediction:[ 0.721]
True Pred:[1] Encrypted Prediction:[23090462896.17028 0.0] Decrypted Prediction:[ 0.688]
True Pred:[0] Encrypted Prediction:[1748380342.4553354 0.0] Decrypted Prediction:[ 0.052]
Выше приведен результат, который я получаю при обучении этой сети. Из-за шума шифрования и низкой точности настройка модели затруднена, а обучение модели происходит медленно из-за накладных расходов на операции шифрования и дешифрования. Хотя я также хочу сделать несколько более простых примеров, начиная с нашей темы и концепции, первое, что нужно обеспечить, — это безопасность схемы.
Особенности:
- Все веса сети зашифрованы.
- Данные обучения не зашифрованы.
- После обучения сеть можно расшифровать для повышения производительности или переобучить (или переключить на другой ключ шифрования).
- И потеря при обучении, и прогноз на выходе являются зашифрованными значениями. Мы должны расшифровать их, чтобы объяснить поведение сети.
Часть 9: Классификация настроений
Чтобы представить несколько более реалистичных сценариев, вот сеть классификации настроений, реализованная на основе сети в Udacity Nanodegree, которая обучена на рейтингах IMDB. ты сможешьздесьнайти полный код
import time
import sys
import numpy as np
# 让我们调整之前的网络来适配这个问题
class SentimentNetwork:
def __init__(self, reviews,labels,min_count = 10,polarity_cutoff = 0.1,hidden_nodes = 8, learning_rate = 0.1):
np.random.seed(1234)
self.pre_process_data(reviews, polarity_cutoff, min_count)
self.init_network(len(self.review_vocab),hidden_nodes, 1, learning_rate)
def pre_process_data(self,reviews, polarity_cutoff,min_count):
print("Pre-processing data...")
positive_counts = Counter()
negative_counts = Counter()
total_counts = Counter()
for i in range(len(reviews)):
if(labels[i] == 'POSITIVE'):
for word in reviews[i].split(" "):
positive_counts[word] += 1
total_counts[word] += 1
else:
for word in reviews[i].split(" "):
negative_counts[word] += 1
total_counts[word] += 1
pos_neg_ratios = Counter()
for term,cnt in list(total_counts.most_common()):
if(cnt >= 50):
pos_neg_ratio = positive_counts[term] / float(negative_counts[term]+1)
pos_neg_ratios[term] = pos_neg_ratio
for word,ratio in pos_neg_ratios.most_common():
if(ratio > 1):
pos_neg_ratios[word] = np.log(ratio)
else:
pos_neg_ratios[word] = -np.log((1 / (ratio + 0.01)))
review_vocab = set()
for review in reviews:
for word in review.split(" "):
if(total_counts[word] > min_count):
if(word in pos_neg_ratios.keys()):
if((pos_neg_ratios[word] >= polarity_cutoff) or (pos_neg_ratios[word] <= -polarity_cutoff)):
review_vocab.add(word)
else:
review_vocab.add(word)
self.review_vocab = list(review_vocab)
label_vocab = set()
for label in labels:
label_vocab.add(label)
self.label_vocab = list(label_vocab)
self.review_vocab_size = len(self.review_vocab)
self.label_vocab_size = len(self.label_vocab)
self.word2index = {}
for i, word in enumerate(self.review_vocab):
self.word2index[word] = i
self.label2index = {}
for i, label in enumerate(self.label_vocab):
self.label2index[label] = i
def init_network(self, input_nodes, hidden_nodes, output_nodes, learning_rate):
# 设置输入层,隐藏层和输出层的节点数
self.input_nodes = input_nodes
self.hidden_nodes = hidden_nodes
self.output_nodes = output_nodes
print("Initializing Weights...")
self.weights_0_1_t = np.zeros((self.input_nodes,self.hidden_nodes))
self.weights_1_2_t = np.random.normal(0.0, self.output_nodes**-0.5,
(self.hidden_nodes, self.output_nodes))
print("Encrypting Weights...")
self.weights_0_1 = list()
for i,row in enumerate(self.weights_0_1_t):
sys.stdout.write("\rEncrypting Weights from Layer 0 to Layer 1:" + str(float((i+1) * 100) / len(self.weights_0_1_t))[0:4] + "% done")
self.weights_0_1.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
print("")
self.weights_1_2 = list()
for i,row in enumerate(self.weights_1_2_t):
sys.stdout.write("\rEncrypting Weights from Layer 1 to Layer 2:" + str(float((i+1) * 100) / len(self.weights_1_2_t))[0:4] + "% done")
self.weights_1_2.append(one_way_encrypt_vector(row,scaling_factor).astype('int64'))
self.weights_1_2 = transpose(self.weights_1_2)
self.learning_rate = learning_rate
self.layer_0 = np.zeros((1,input_nodes))
self.layer_1 = np.zeros((1,hidden_nodes))
def sigmoid(self,x):
return 1 / (1 + np.exp(-x))
def sigmoid_output_2_derivative(self,output):
return output * (1 - output)
def update_input_layer(self,review):
# 清除之前的转台,将每层中的值置为0
self.layer_0 *= 0
for word in review.split(" "):
self.layer_0[0][self.word2index[word]] = 1
def get_target_for_label(self,label):
if(label == 'POSITIVE'):
return 1
else:
return 0
def train(self, training_reviews_raw, training_labels):
training_reviews = list()
for review in training_reviews_raw:
indices = set()
for word in review.split(" "):
if(word in self.word2index.keys()):
indices.add(self.word2index[word])
training_reviews.append(list(indices))
layer_1 = np.zeros_like(self.weights_0_1[0])
start = time.time()
correct_so_far = 0
total_pred = 0.5
for i in range(len(training_reviews_raw)):
review_indices = training_reviews[i]
label = training_labels[i]
layer_1 *= 0
for index in review_indices:
layer_1 += self.weights_0_1[index]
layer_1 = layer_1 / float(len(review_indices))
layer_1 = layer_1.astype('int64') # round to nearest integer
layer_2 = sigmoid(innerProd(layer_1,self.weights_1_2[0],M_onehot[len(layer_1) - 2][1],l) / float(scaling_factor))[0:2]
if(label == 'POSITIVE'):
layer_2_delta = layer_2 - (c_ones[len(layer_2) - 2] * scaling_factor)
else:
layer_2_delta = layer_2
weights_1_2_trans = transpose(self.weights_1_2)
layer_1_delta = mat_mul_forward(layer_2_delta,weights_1_2_trans,scaling_factor).astype('int64')
self.weights_1_2 -= np.array(outer_product(layer_2_delta,layer_1)) * self.learning_rate
for index in review_indices:
self.weights_0_1[index] -= (layer_1_delta * self.learning_rate).astype('int64')
# 我们希望这里同时可以进行解密以便我们观察网络训练情况
total_pred += (s_decrypt(layer_2)[0] / scaling_factor)
if((s_decrypt(layer_2)[0] / scaling_factor) >= (total_pred / float(i+2)) and label == 'POSITIVE'):
correct_so_far += 1
if((s_decrypt(layer_2)[0] / scaling_factor) < (total_pred / float(i+2)) and label == 'NEGATIVE'):
correct_so_far += 1
reviews_per_second = i / float(time.time() - start)
sys.stdout.write("\rProgress:" + str(100 * i/float(len(training_reviews_raw)))[:4] + "% Speed(reviews/sec):" + str(reviews_per_second)[0:5] + " #Correct:" + str(correct_so_far) + " #Trained:" + str(i+1) + " Training Accuracy:" + str(correct_so_far * 100 / float(i+1))[:4] + "%")
if(i % 100 == 0):
print(i)
def test(self, testing_reviews, testing_labels):
correct = 0
start = time.time()
for i in range(len(testing_reviews)):
pred = self.run(testing_reviews[i])
if(pred == testing_labels[i]):
correct += 1
reviews_per_second = i / float(time.time() - start)
sys.stdout.write("\rProgress:" + str(100 * i/float(len(testing_reviews)))[:4] \
+ "% Speed(reviews/sec):" + str(reviews_per_second)[0:5] \
+ "% #Correct:" + str(correct) + " #Tested:" + str(i+1) + " Testing Accuracy:" + str(correct * 100 / float(i+1))[:4] + "%")
def run(self, review):
# 输入层
# 隐藏层
self.layer_1 *= 0
unique_indices = set()
for word in review.lower().split(" "):
if word in self.word2index.keys():
unique_indices.add(self.word2index[word])
for index in unique_indices:
self.layer_1 += self.weights_0_1[index]
# 输出层
layer_2 = self.sigmoid(self.layer_1.dot(self.weights_1_2))
if(layer_2[0] >= 0.5):
return "POSITIVE"
else:
return "NEGATIVE"
Progress:0.0% Speed(reviews/sec):0.0 #Correct:1 #Trained:1 Training Accuracy:100.%0
Progress:0.41% Speed(reviews/sec):1.978 #Correct:66 #Trained:101 Training Accuracy:65.3%100
Progress:0.83% Speed(reviews/sec):2.014 #Correct:131 #Trained:201 Training Accuracy:65.1%200
Progress:1.25% Speed(reviews/sec):2.011 #Correct:203 #Trained:301 Training Accuracy:67.4%300
Progress:1.66% Speed(reviews/sec):2.003 #Correct:276 #Trained:401 Training Accuracy:68.8%400
Progress:2.08% Speed(reviews/sec):2.007 #Correct:348 #Trained:501 Training Accuracy:69.4%500
Progress:2.5% Speed(reviews/sec):2.015 #Correct:420 #Trained:601 Training Accuracy:69.8%600
Progress:2.91% Speed(reviews/sec):1.974 #Correct:497 #Trained:701 Training Accuracy:70.8%700
Progress:3.33% Speed(reviews/sec):1.973 #Correct:581 #Trained:801 Training Accuracy:72.5%800
Progress:3.75% Speed(reviews/sec):1.976 #Correct:666 #Trained:901 Training Accuracy:73.9%900
Progress:4.16% Speed(reviews/sec):1.983 #Correct:751 #Trained:1001 Training Accuracy:75.0%1000
Progress:4.33% Speed(reviews/sec):1.940 #Correct:788 #Trained:1042 Training Accuracy:75.6%
Часть 10: Преимущества шифрования данных
Реализация, аналогичная этой статье, состоит в том, чтобы сеть обучалась на зашифрованных данных и выводила зашифрованные предсказания. Это хорошая идея, но есть еще некоторые недостатки. Во-первых, зашифрованные данные означают, что данные не имеют смысла для кого-то без закрытого ключа, что делает невозможным обучение личных исходных данных в той же сети глубокого обучения. Но большинство коммерческих приложений имеют это требование и нуждаются в агрегировании потребительских данных. Теоретически мы хотим, чтобы каждый потребитель имел свой собственный закрытый ключ, но гомоморфное шифрование требует, чтобы все имели общий закрытый ключ.
Однако зашифрованные сети не страдают от этого ограничения.
С помощью описанного выше метода сначала вы получаете квалифицированную нейронную сеть после обучения в течение определенного периода времени, а затем шифруете открытый ключ и отправляете его А (А может продолжать обучать сеть своими данными в течение определенного периода времени), затем вы забираете нейронную сеть и используете. Другой ключ снова шифруется и отправляется стороне B, и B также может использовать свои данные для обучения сети. Поскольку сама сеть зашифрована, у вас есть полный контроль над сетевым интеллектом.A и B не могут узнать, используют ли они одну и ту же сеть, все происходит без их видения или использования сети. Это позволяет сохранять контроль над интеллектуальной собственностью нейросети, а каждый пользователь сохраняет конфиденциальность собственных данных.
Часть 11: Будущее работы
Появятся более безопасные и быстрые алгоритмы гомоморфного шифрования. Я думаю, что перенос работы на YASHE был бы правильным выбором. Возможно, фреймворк больше подходит пользователям для шифрования из-за сложной абстракции системы. В общем, чтобы эти идеи были реализованы в производственной среде, гомоморфное шифрование должно быть немного быстрее. Однако прогресс в этой области также очень быстрый, и я считаю, что это может быть достигнуто в ближайшем будущем.
Часть 12: Возможные применения
Децентрализованный ИИ:Компании могут развертывать модели непосредственно в полевых условиях для обучения или использования без риска кражи сети.
Защита конфиденциальности потребителей:Упомянутые выше приложения предлагают пользователям возможность контролировать свои данные, они могут «подписаться» на обучение определенных сетей вместо того, чтобы куда-то отправлять все свои данные. Компании также не могут воспользоваться рисками для прав интеллектуальной собственности внутри сети, чтобы отклонить запрос. Данные имеют большое значение, но они должны вернуться к пользователю.
Управляемый сверхразум:Сеть может быть умной, но пока у нее нет закрытого ключа, прогнозы, которые она делает, не могут повлиять на внешний мир.
вопросы и ответы
Как использовать искусственный интеллект для синтеза человеческого голоса?
Связанное Чтение
Практика посадки ИИ в нескольких сценариях
AI Academy | Обзор основ искусственного интеллекта
Эта статья была разрешена автором для публикации в сообществе Tencent Cloud + Для получения дополнительных оригинальных текстов, пожалуйстанажмите
Найдите и подпишитесь на общедоступную учетную запись «Сообщество Yunjia», получите технические галантереи как можно скорее и ответьте на 1024 после подписки, чтобы отправить вам подарочный пакет технических курсов!
Огромный технический практический опыт, все вСообщество Юнцзя!