Раньше, что бы лучше освоить язык программирования, в моем кругу общения считалось, что «программист» должен написать свою реализацию «Блокнота», «Калькулятора», «Экселя» и прочего. Конечно освоив перед этим сортировку пузырьком.
Шли года, менялись задачи. Менялась мода на технологическое направление в IT: разработчик баз данных, web разработка, мобильная разработка, Data майнинг, и вот теперь великий и могучий искусственный интеллект (ИИ). А там где мода — там есть деньги. Ну вы поняли.
Признаюсь, у меня было много попыток войти в айти понять что же такое ИИ, что там внутри, и как это работает. Всегда поражали люди которые поднимают языковые модели на своем компьютере, настраивают какие-то там параметры, формируют какие-то промпты, и «это» даже отвечает им что-то вразумительное.
Что такое ИИ — можно почитать в википедии. Это очень обширная тема. Давайте прикоснемся с частичке ИИ, непосредственно — к перцептрону.
Лиха беда начало, попробуем сделать что-то рабочее.
Реализация
Итак, наша цель: создать многослойный перцептрон, умеющий складывать, вычитать, умножать и делить два числа.
Сначала была мысль написать свою реализацию. Но примерно представив насколько это может затянутся — мысль была категорически отвергнута.
На просторах гитхаба наткнулся на проект с примерами https://github.com/Cr33zz/Neuro
Прежде чем складывать, вычитать и т.д. попробуем как это будет работать на примере битовой операции «ИЛИ» (OR).
//обучающие данные - битовая операция ИЛИ
List<Data> trainingData = new List<Data>()
{
new Data(new Tensor(new float[] { 0, 0 }, new Shape(2)), new Tensor(new float[] {0 }, new Shape(1))),
new Data(new Tensor(new float[] { 0, 1 }, new Shape(2)), new Tensor(new float[] {1 }, new Shape(1))),
new Data(new Tensor(new float[] { 1, 0 }, new Shape(2)), new Tensor(new float[] {1 }, new Shape(1))),
new Data(new Tensor(new float[] { 1, 1 }, new Shape(2)), new Tensor(new float[] {1 }, new Shape(1)))
};
//создаем сеть
var net = new NeuralNetwork("simple_net_or_test");
//создаем модель
var model = new Sequential();
//добавляем входящий слой - 2 входящих числа
model.AddLayer(new Flatten(new Shape(2)));
//внутренний слой
model.AddLayer(new Dense(model.LastLayer, 4, Activation.ReLU));
//выходной слой - 1 число, результат
model.AddLayer(new Dense(model.LastLayer, 1, Activation.Linear));
net.Model = model;
//оптимизация
net.Optimize(new SGD(), Loss.MeanSquareError);
//обучение сети, 150 эпох
net.Fit(trainingData, 1, 150, null, 0, Track.Nothing);
//проверяем
for (int i = 0; i < trainingData.Count; ++i)
{
var inp = string.Join(" ", trainingData[i].Input.GetValues());
//даем сети входные данные, и получаем результат
var predict = net.Predict(trainingData[i].Input).First().GetValues().First();
var outp = string.Join(" ", predict);
Console.WriteLine("{0} = {1} ({2})", inp, Math.Round(predict), outp);
}
Результат:
0 0 = 0 (0,3498991)
1 1 = 1 (1,176591)
0 1 = 1 (0,7329522)
1 0 = 1 (0,7935382)
Результат выдается «примерный», поэтому округляем в большую сторону с помощью Math.Round
. В скобках - тот самый результат который выдает сеть.
Сложение, вычитание
А теперь попробуем сложение. Сгенерируем обучающие данные: таблицу сложения от 0 до 9. Сеть оставим без изменений.
//обучающие данные
List<Data> trainingData = new List<Data>();
for (int a = 0; a <= 9; ++a)
{
for (int b = 0; b <= 9; ++b)
{
trainingData.Add(new Data(new Tensor(new float[] { a, b }, new Shape(2)), new Tensor(new float[] { a + b }, new Shape(1))));
}
}
Результат:
0+0 = 0 (0,3612165)
7+4 = 11 (11,02726)
3+4 = 7 (7,00885)
1+5 = 6 (6,00395)
2+8 = 10 (10,02146)
4+9 = 13 (13,03497)
1+7 = 8 (8,012558)
9+4 = 13 (13,03646)
1+6 = 7 (7,008254)
6+3 = 9 (9,018351)
0+9 = 9 (9,016563)
2+1 = 3 (2,991335)
4+5 = 9 (9,017755)
4+0 = 4 (3,996235)
1+4 = 5 (4,999646)
3+9 = 12 (12,03037)
7+5 = 12 (12,03156)
8+1 = 9 (9,018947)
1+8 = 9 (9,016861)
....и т.д.
Для вычитания необходимо заменить только входящие данные. Сеть оставим без изменений.
Умножение, деление
А вот с умножением не все так просто. Поэкспериментировав с конфигурацией многослойного персептрона, у меня получился рабочий результат при такой реализации:
//обучающие данные - таблица умножения от 0 до 9
List<Data> trainingData = new List<Data>();
for (int a = 0; a <= 9; ++a)
{
for (int b = 0; b <= 9; ++b)
{
trainingData.Add(new Data(new Tensor(new float[] { a, b }, new Shape(2)), new Tensor(new float[] { a * b }, new Shape(1))));
}
}
var net = new NeuralNetwork("simple_net_mul_test");
var model = new Sequential();
model.AddLayer(new Flatten(new Shape(2)));
model.AddLayer(new Dense(model.LastLayer, 120, Activation.Sigmoid));
model.AddLayer(new Dense(model.LastLayer, 64, Activation.Sigmoid));
model.AddLayer(new Dense(model.LastLayer, 1, Activation.Linear));
net.Model = model;
net.Optimize(new SGD(), Loss.MeanSquareError);
net.Fit(trainingData, 4, 1200, null, 1, Track.Nothing);
for (int i = 0; i < trainingData.Count; ++i)
{
var inp = string.Join("*", trainingData[i].Input.GetValues());
var need = trainingData[i].Output.GetValues().First();
var predict = net.Predict(trainingData[i].Input).First().GetValues().First();
var outp = string.Join(" ", predict);
Console.WriteLine("{0} = {1} ({2}) {3}", inp, Math.Round(predict), outp, need);
}
Обучение при такой конфигурации на моем ПК занимает достаточно много времени. Можно успеть выпеть кофейку. Насчет деления - вопрос остается открытым. Дальше в тексте будет понятно почему.
Состояние сети
Погодите. Мы же не собираемся после запуска калькулятора ждать, пока он обучит сети сложению, вычитанию, умножению и делению, прежде чем собственно начать калькулировать. Нам нужно сохранять состояние обученной сети. И для этого есть замечательный метод net.SaveStateXml("file.xml")
. А для загрузки состояния сети есть метод net.LoadStateXml("file.xml")
.
Теперь, вместо долгого и мучительного обучения можно сделать так:
var net = new NeuralNetwork("simple_net_mul_test");
var model = new Sequential();
model.AddLayer(new Flatten(new Shape(2)));
model.AddLayer(new Dense(model.LastLayer, 120, Activation.Sigmoid));
model.AddLayer(new Dense(model.LastLayer, 64, Activation.Sigmoid));
model.AddLayer(new Dense(model.LastLayer, 1, Activation.Linear));
net.Model = model;
//загружаем состояние сети
net.LoadStateXml("mul.xml");
//2 * 3
var example = new Tensor(new float[] { 2, 3 }, new Shape(2));
//равно 5.808744
var result = net2.Predict(example).First().GetValues().First();
//округляем до 6
Console.WriteLine(Math.Round(result));
Согласен, выглядит многословно. Мы должны сформировать сеть добавляя нужные слои AddLayer
, т.к. метод net.LoadStateXml("file.xml")
не загружает сеть с нуля, он только восстанавливает состояние сети из файла. Но это поправимо. Позже я добавлю метод net.LoadStateXml2("file.xml")
который полностью загрузит всю конфигурацию сети.
Длинная арифметика
Казалось бы, мы на финишной прямой. Наша сеть умеет складывать, вычитать, умножать... Но только в пределах от 0 до 9. Какой же это калькулятор если считает числа в таком небольшом диапазоне?
Надо увеличить диапазон! До десятков, до сотен, до тысяч... Но тогда надо увеличивать размерность сети, дольше обучать, "раздувать" сеть. Это не наш путь.
Нам поможет "Длинная арифметика", где все арифметические операции выполняются с числами произвольной величины. А внутри подсчет идёт максимум десятками. Помните умножение столбиком? Это нам и нужно: переучивать сеть не нужно, оставим всё как есть.
В .Net есть реализация длинной арифметики - класс BigInteger
, но мы не можем просто так вмешаться в нутро, и заменить арифметические операции своими, так сказать, нейросетевыми. Поэтому путём недолгого гугления был найден класс BigNumber.
Изучая код этого класса, выяснилось, что операция деления выполняется с помощью операций сложения, вычитания и умножения. Т.е. достаточно всего три операции для деления, которые у нас уже есть!
Теперь мы можем делать так:
Console.WriteLine(new BigNumber(10) + new BigNumber(5)); //15
Console.WriteLine(new BigNumber(10) - new BigNumber(5)); //5
Console.WriteLine(new BigNumber(10) * new BigNumber(5)); //50
Console.WriteLine(new BigNumber(10) / new BigNumber(5)); //2
Console.WriteLine(new BigNumber(555) * new BigNumber(777));//431235
Полный код переработанного BigNumber
using Neuro.Models;
using Neuro.Tensors;
using Neuro;
using System.Text;
using System.Collections.Generic;
using System;
using System.Linq;
namespace BigInteger
{
public enum Sign
{
Minus = -1,
Plus = 1
}
public delegate int SummaryHandler(int a, int b);
public delegate int SubstractHandler(int a, int b);
public delegate int MultiplyHandler(int a, int b);
public class BigNumber
{
public static SummaryHandler summary;
public static SummaryHandler substract;
public static MultiplyHandler multiply;
private readonly List<byte> digits = new List<byte>();
private static NeuralNetwork prepareSUM()
{
var net = new NeuralNetwork("simple_net_calc_sum_test");
net.LoadStateXml2("sum.xml");
return net;
}
private static NeuralNetwork prepareSUB()
{
var net = new NeuralNetwork("simple_net_calc_sub_test");
net.LoadStateXml2("sub.xml");
return net;
}
private static NeuralNetwork prepareMUL()
{
var net = new NeuralNetwork("simple_net_calc_mul_test");
net.LoadStateXml2("mul.xml");
return net;
}
private static int calcNeuro(NeuralNetwork net, int a, int b)
{
var tens = new Tensor(new float[] { a, b }, new Shape(2));
var predict = net.Predict(tens).First().GetValues().First();
return (int)Math.Round(predict);
}
static BigNumber() {
var sum = prepareSUM();
var sub = prepareSUB();
var mul = prepareMUL();
summary = (int a, int b) =>
{
//return a + b;
return calcNeuro(sum, a, b);
};
substract = (int a, int b) =>
{
//return a - b;
return calcNeuro(sub, a, b);
};
multiply = (int a, int b) =>
{
//return a * b;
return calcNeuro(mul, a, b);
};
}
public BigNumber(List<byte> bytes)
{
digits = bytes.ToList();
RemoveNulls();
}
public BigNumber(Sign sign, List<byte> bytes)
{
Sign = sign;
digits = bytes;
RemoveNulls();
}
public BigNumber(string s)
{
if (s.StartsWith("-"))
{
Sign = Sign.Minus;
s = s.Substring(1);
}
foreach (var c in s.Reverse())
{
digits.Add(Convert.ToByte(c.ToString()));
}
RemoveNulls();
}
public BigNumber(uint x) => digits.AddRange(GetBytes(x));
public BigNumber(int x)
{
if (x < 0)
{
Sign = Sign.Minus;
}
digits.AddRange(GetBytes((uint)Math.Abs(x)));
}
/// <summary>
/// метод для получения списка цифр из целого беззнакового числа
/// </summary>
/// <param name="num"></param>
/// <returns></returns>
private List<byte> GetBytes(uint num)
{
var bytes = new List<byte>();
do
{
bytes.Add((byte)(num % 10));
num /= 10;
} while (num > 0);
return bytes;
}
/// <summary>
/// метод для удаления лидирующих нулей длинного числа
/// </summary>
private void RemoveNulls()
{
for (var i = digits.Count - 1; i > 0; i--)
{
if (digits[i] == 0)
{
digits.RemoveAt(i);
}
else
{
break;
}
}
}
/// <summary>
/// метод для получения больших чисел формата valEexp(пример 1E3 = 1000)
/// </summary>
/// <param name="val">значение числа</param>
/// <param name="exp">экспонента(количество нулей после значения val)</param>
/// <returns></returns>
public static BigNumber Exp(byte val, int exp)
{
var bigInt = Zero;
bigInt.SetByte(exp, val);
bigInt.RemoveNulls();
return bigInt;
}
public static BigNumber Zero => new BigNumber(0);
public static BigNumber One => new BigNumber(1);
//длина числа
public int Size => digits.Count;
//знак числа
public Sign Sign { get; private set; } = Sign.Plus;
//получение цифры по индексу
public byte GetByte(int i) => i < Size ? digits[i] : (byte)0;
//установка цифры по индексу
public void SetByte(int i, byte b)
{
while (digits.Count <= i)
{
digits.Add(0);
}
digits[i] = b;
}
//преобразование длинного числа в строку
public override string ToString()
{
if (this == Zero) return "0";
var s = new StringBuilder(Sign == Sign.Plus ? "" : "-");
for (int i = digits.Count - 1; i >= 0; i--)
{
s.Append(Convert.ToString(digits[i]));
}
return s.ToString();
}
#region Методы арифметических действий над большими числами
private static BigNumber Add(BigNumber a, BigNumber b)
{
var digits = new List<byte>();
var maxLength = Math.Max(a.Size, b.Size);
byte t = 0;
for (int i = 0; i < maxLength; i++)
{
//byte sum = (byte)(a.GetByte(i) + b.GetByte(i) + t);
byte sum = (byte)(summary(a.GetByte(i), b.GetByte(i)) + t);
if (sum >= 10)
{
sum -= 10;
t = 1;
}
else
{
t = 0;
}
digits.Add(sum);
}
if (t > 0)
{
digits.Add(t);
}
return new BigNumber(a.Sign, digits);
}
private static BigNumber Substract(BigNumber a, BigNumber b)
{
var digits = new List<byte>();
BigNumber max = Zero;
BigNumber min = Zero;
//сравниваем числа игнорируя знак
var compare = Comparison(a, b, ignoreSign: true);
switch (compare)
{
case -1:
min = a;
max = b;
break;
case 0:
return Zero;
case 1:
min = b;
max = a;
break;
}
//из большего вычитаем меньшее
var maxLength = Math.Max(a.Size, b.Size);
var t = 0;
for (var i = 0; i < maxLength; i++)
{
//var s = max.GetByte(i) - min.GetByte(i) - t;
var s = substract(max.GetByte(i),min.GetByte(i)) - t;
if (s < 0)
{
s += 10;
t = 1;
}
else
{
t = 0;
}
digits.Add((byte)s);
}
return new BigNumber(max.Sign, digits);
}
private static BigNumber Multiply(BigNumber a, BigNumber b)
{
var retValue = Zero;
for (var i = 0; i < a.Size; i++)
{
for (int j = 0, carry = 0; (j < b.Size) || (carry > 0); j++)
{
//var cur = retValue.GetByte(i + j) + a.GetByte(i) * b.GetByte(j) + carry;
var cur = retValue.GetByte(i + j) + multiply(a.GetByte(i), b.GetByte(j)) + carry;
retValue.SetByte(i + j, (byte)(cur % 10));
carry = cur / 10;
}
}
retValue.Sign = a.Sign == b.Sign ? Sign.Plus : Sign.Minus;
return retValue;
}
private static BigNumber Div(BigNumber a, BigNumber b)
{
var retValue = Zero;
var curValue = Zero;
for (var i = a.Size - 1; i >= 0; i--)
{
curValue += Exp(a.GetByte(i), i);
var x = 0;
var l = 0;
var r = 10;
while (l <= r)
{
var m = (l + r) / 2;
var cur = b * Exp((byte)m, i);
if (cur <= curValue)
{
x = m;
l = m + 1;
}
else
{
r = m - 1;
}
}
retValue.SetByte(i, (byte)(x % 10));
var t = b * Exp((byte)x, i);
curValue = curValue - t;
}
retValue.RemoveNulls();
retValue.Sign = a.Sign == b.Sign ? Sign.Plus : Sign.Minus;
return retValue;
}
private static BigNumber Mod(BigNumber a, BigNumber b)
{
var retValue = Zero;
for (var i = a.Size - 1; i >= 0; i--)
{
retValue += Exp(a.GetByte(i), i);
var x = 0;
var l = 0;
var r = 10;
while (l <= r)
{
var m = (l + r) >> 1;
var cur = b * Exp((byte)m, i);
if (cur <= retValue)
{
x = m;
l = m + 1;
}
else
{
r = m - 1;
}
}
retValue -= b * Exp((byte)x, i);
}
retValue.RemoveNulls();
retValue.Sign = a.Sign == b.Sign ? Sign.Plus : Sign.Minus;
return retValue;
}
#endregion
#region Методы для сравнения больших чисел
private static int Comparison(BigNumber a, BigNumber b, bool ignoreSign = false)
{
return CompareSign(a, b, ignoreSign);
}
private static int CompareSign(BigNumber a, BigNumber b, bool ignoreSign = false)
{
if (!ignoreSign)
{
if (a.Sign < b.Sign)
{
return -1;
}
else if (a.Sign > b.Sign)
{
return 1;
}
}
return CompareSize(a, b);
}
private static int CompareSize(BigNumber a, BigNumber b)
{
if (a.Size < b.Size)
{
return -1;
}
else if (a.Size > b.Size)
{
return 1;
}
return CompareDigits(a, b);
}
private static int CompareDigits(BigNumber a, BigNumber b)
{
var maxLength = Math.Max(a.Size, b.Size);
for (var i = maxLength; i >= 0; i--)
{
if (a.GetByte(i) < b.GetByte(i))
{
return -1;
}
else if (a.GetByte(i) > b.GetByte(i))
{
return 1;
}
}
return 0;
}
#endregion
#region Арифметические операторы
// унарный минус(изменение знака числа)
public static BigNumber operator -(BigNumber a)
{
a.Sign = a.Sign == Sign.Plus ? Sign.Minus : Sign.Plus;
return a;
}
//сложение
public static BigNumber operator +(BigNumber a, BigNumber b) => a.Sign == b.Sign
? Add(a, b)
: Substract(a, b);
//вычитание
public static BigNumber operator -(BigNumber a, BigNumber b) => a + -b;
//умножение
public static BigNumber operator *(BigNumber a, BigNumber b) => Multiply(a, b);
//целочисленное деление(без остатка)
public static BigNumber operator /(BigNumber a, BigNumber b) => Div(a, b);
//остаток от деления
public static BigNumber operator %(BigNumber a, BigNumber b) => Mod(a, b);
#endregion
#region Операторы сравнения
public static bool operator <(BigNumber a, BigNumber b) => Comparison(a, b) < 0;
public static bool operator >(BigNumber a, BigNumber b) => Comparison(a, b) > 0;
public static bool operator <=(BigNumber a, BigNumber b) => Comparison(a, b) <= 0;
public static bool operator >=(BigNumber a, BigNumber b) => Comparison(a, b) >= 0;
public static bool operator ==(BigNumber a, BigNumber b) => Comparison(a, b) == 0;
public static bool operator !=(BigNumber a, BigNumber b) => Comparison(a, b) != 0;
public override bool Equals(object obj) => !(obj is BigNumber) ? false : this == (BigNumber)obj;
#endregion
}
}
Батонокидание
Осталось нарисовать калькулятор похожий на встроенный калькулятор Windows. Скинуть в папку с .exe файлом сохраненные состояния трех сетей (файлы: sum.xml, sub.xml, mul.xml) и вычислять.
frmNeuroCalc
using System;
using System.Windows.Forms;
using BigInteger;
namespace SimpleNeuro
{
public partial class frmNeuroCalc : Form
{
BigNumber lastValue;
string lastOperation = "=";
bool lastDigit = false;
public frmNeuroCalc()
{
InitializeComponent();
}
private void frmNeuroCalc_Load(object sender, EventArgs e)
{
try
{
lastValue = new BigNumber(0);
} catch (Exception ex)
{
MessageBox.Show(ex.InnerException.Message);
}
textBox_Result.Text = lastValue.ToString();
}
private void numbers_Click(object sender, EventArgs e)
{
if (sender is Button)
{
var text = (sender as Button).Text;
if (!lastDigit)
{
textBox_Result.Text = "";
}
lastDigit = true;
if (textBox_Result.Text.Trim() == "0")
textBox_Result.Text = text;
else
textBox_Result.AppendText(text);
}
}
private void btnC_Click(object sender, EventArgs e)
{
//очищаем всё
lastValue = new BigNumber(0);
textBox_Result.Text = lastValue.ToString();
lastOperation = "";
}
private void btnCE_Click(object sender, EventArgs e)
{
//очистка только текущего значения
textBox_Result.Text = "0";
}
private void operation_Click(object sender, EventArgs e)
{
if (sender is Button)
{
var operation = (sender as Button).Text;
var currentValue = textBox_Result.Text;
var resultValue = new BigNumber(0);
if (operation == "=")
{
switch (lastOperation)
{
case "+":
resultValue = lastValue + new BigNumber(currentValue);
break;
case "-":
resultValue = lastValue - new BigNumber(currentValue);
break;
case "*":
resultValue = lastValue * new BigNumber(currentValue);
break;
case "/":
resultValue = lastValue / new BigNumber(currentValue);
break;
}
textBox_Result.Text = resultValue.ToString();
} else
{
lastOperation = operation;
lastValue = new BigNumber(currentValue);
lastDigit = false;
}
}
}
//игнорируем ввод всех символов кроме цифр
private void textBox_Result_KeyPress(object sender, KeyPressEventArgs e)
{
e.Handled = !char.IsDigit(e.KeyChar) && !char.IsControl(e.KeyChar);
}
}
}
Исходный код можно посмотреть по ссылке: SimpleNeuro
Готовый релиз: Release v1.0.0
NeuroTeach - консольное приложение по обучению и сохранению состояния сети.
NeuroCalc - калькулятор.
NeuroLibrary - библиотека классов, содержащая NeuralNetwork и BigNumber.
Итог
Путем объединения персептрона и длинной арифметики мы можем устроить настоящую числодробилку. Конечно при такой реализации, ресурсы компьютера больше тратятся на работу сети чем на несложные арифметические вычисления.
Что-бы выполнить net.Predict(...)
производится намного больше арифметических операций, чем одна операция умножения например:
где-то внутри FeedForward
public virtual void Mul(bool transposeT1, bool transposeT2, Tensor t1, Tensor t2, Tensor result)
{
var t1Temp = transposeT1 ? t1.Transposed() : t1;
var t2Temp = transposeT2 ? t2.Transposed() : t2;
t1Temp.CopyToHost();
t2Temp.CopyToHost();
result.Zero();
int N = t1Temp.Height;
int M = t2Temp.Width;
int K = t1Temp.Width;
for (int n = 0; n < result.BatchSize; ++n)
{
int t1N = Math.Min(n, t1Temp.BatchSize - 1);
int t2N = Math.Min(n, t2Temp.BatchSize - 1);
for (int d = 0; d < t1Temp.Depth; ++d)
for (int i = 0; i < N; ++i)
for (int j = 0; j < M; ++j)
for (int k = 0; k < K; ++k)
result[j, i, d, n] += t1Temp[k, i, d, t1N] * t2Temp[j, k, d, t2N];
}
}
Кроме того, наш калькулятор не умеет считать числа с плавающей запятой (заметили неактивную кнопку с запятой на калькуляторе?). Вам домашнее задание: реализовать вычисления с плавающей запятой, возведение в квадрат, в куб, получение корня, логарифма, тангенса...
Немного юмора
Не дай бог Microsoft когда-нибудь заменит стандартный калькулятор на нейрокалькулятор, который сломается после очередного обновления, из-за ошибки в файле хранящем состояние персептрона.
А если без шуток, даю идею: в массиве нейросети можно хранить и передавать по интернету какую-либо логику (в том числе и бизнес-логику), типа зашитой формулы вычисления чего-либо, либо алгоритм обработки специфических данных. И никто из тех кто перехватил этот массив чисел не сможет даже понять что с этим делать.
Всем спасибо за чтение!
Комментарии (3)
AndyGhost
22.01.2025 07:32Круто! спасибо за ссылки на код, интересно посмотреть. Буквально на днях тоже сделал свой калькулятор на нейросети. При чем шел почти такими же шагами - сначала сделал логический калькулятор, чтобы убедиться, что можно действительно обучить любой логической операции, а потом арифметический. саму сеть делал сам, чтобы разобраться в деталях, как оно устроено и работает - перемножение матриц, векторов и тд. очень затягивает поэкспериментировать, чему еще можно научить.
AdrianoVisoccini
22.01.2025 07:32А я все думал, как продать калькулятор в 2025? Теперь то бизнес пойдет
profotocor
Ну я думаю что к концу года примерно все калькуляторы будут работать так, плюсом требовать наличие аппаратных ядер CUDA. Джунам не показывайте!