۳۰ شهریور ۱۴۰۳

Techboy

اخبار و اطلاعات روز تکنولوژی

نحوه ساخت شبکه عصبی در جاوا

بهترین راه برای درک شبکه های عصبی این است که برای خودتان یکی بسازید. بیایید با ایجاد و آموزش یک شبکه عصبی در جاوا شروع کنیم.

بهترین راه برای درک شبکه های عصبی این است که برای خودتان یکی بسازید. بیایید با ایجاد و آموزش یک شبکه عصبی در جاوا شروع کنیم.

شبکه های عصبی مصنوعی شکلی از یادگیری عمیق و یکی از ستون های هوش مصنوعی مدرن هستند. بهترین راه برای درک واقعی نحوه کار این چیزها، ساختن آن است. این مقاله مقدمه ای عملی برای ساخت و آموزش شبکه عصبی در جاوا خواهد بود.

به مقاله قبلی من، سبک های یادگیری ماشینی: مقدمه ای بر شبکه های عصبی مراجعه کنید تا مروری بر نحوه عملکرد شبکه های عصبی مصنوعی داشته باشید. مثال ما برای این مقاله به هیچ وجه یک سیستم درجه تولید نیست. در عوض، تمام اجزای اصلی را در یک نسخه نمایشی نشان می دهد که برای درک آسان طراحی شده است.

یک شبکه عصبی پایه

شبکه عصبی نموداری از گره‌هایی است که نرون‌ها نامیده می‌شوند. نورون واحد اصلی محاسبات است. ورودی ها را دریافت کرده و با استفاده از الگوریتم وزن در هر ورودی، بایاس در هر گره و پردازشگر تابع نهایی (معروف به تابع فعال سازی) آنها را پردازش می کند. می توانید یک نورون دو ورودی را که در شکل ۱ نشان داده شده است ببینید.

نمودار یک شبکه عصبی برای یادگیری ماشین.

شکل ۱. یک نورون دو ورودی در یک شبکه عصبی.

این مدل دارای طیف وسیعی از تنوع است، اما ما از این پیکربندی دقیق برای نسخه نمایشی استفاده خواهیم کرد.

اولین گام ما مدل سازی کلاس Neuron است که این مقادیر را نگه می دارد. می‌توانید کلاس Neuron را در فهرست ۱ ببینید. توجه داشته باشید که این اولین نسخه از کلاس است. با افزودن قابلیت تغییر خواهد کرد.


class Neuron {
    Random random = new Random();
    private Double bias = random.nextDouble(-1, 1); 
    public Double weight1 = random.nextDouble(-1, 1); 
    private Double weight2 = random.nextDouble(-1, 1);
   
    public double compute(double input1, double input2){
      double preActivation = (this.weight1 * input1) + (this.weight2 * input2) + this.bias;
      double output = Util.sigmoid(preActivation);
      return output;
    }
  }

می توانید ببینید که کلاس Neuron بسیار ساده است و دارای سه عضو است: bias، weight1 و weight2. هر عضو به یک دوبل تصادفی بین -۱ و ۱ مقداردهی اولیه می شود.

هنگامی که خروجی نورون را محاسبه می کنیم، از الگوریتم نشان داده شده در شکل ۱ پیروی می کنیم: هر ورودی را در وزن آن ضرب می کنیم، به اضافه بایاس: input1 *weight1 + input2 *weight2 + bias. این محاسبه پردازش نشده را به ما می دهد (یعنی preActivation) که از طریق تابع فعال سازی اجرا می کنیم. در این مورد، ما از عملکرد فعال‌سازی سیگموئید استفاده می‌کنیم که مقادیر را در محدوده ۱- تا ۱ فشرده می‌کند. فهرست ۲ روش ثابت Util.sigmoid() را نشان می دهد.


public class Util {
  public static double sigmoid(double in){
    return 1 / (1 + Math.exp(-in));
  }
}

اکنون که دیدیم نورون‌ها چگونه کار می‌کنند، اجازه دهید تعدادی نورون را در یک شبکه قرار دهیم. ما از کلاس Network با فهرستی از نورون‌ها همانطور که در فهرست ۳ نشان داده شده است استفاده خواهیم کرد.


class Network {
    List<Neuron> neurons = Arrays.asList(
      new Neuron(), new Neuron(), new Neuron(), /* input nodes */
      new Neuron(), new Neuron(),               /* hidden nodes */
      new Neuron());                            /* output node */
    }
}

اگرچه لیست نورون‌ها یک بعدی است، اما در حین استفاده آن‌ها را به هم متصل می‌کنیم تا یک شبکه تشکیل دهند. سه نورون اول ورودی هستند، دومی و سومی پنهان هستند و آخرین مورد گره خروجی است.

پیش‌بینی کنید

اکنون، بیایید از شبکه برای پیش بینی استفاده کنیم. ما از یک مجموعه داده ساده شامل دو عدد صحیح ورودی و قالب پاسخ ۰ تا ۱ استفاده می کنیم. مثال من از ترکیب وزن-قد برای حدس زدن جنسیت یک فرد بر اساس این فرض استفاده می کند که وزن و قد بیشتر نشان می دهد یک فرد مذکر است. . ما می توانیم از همان فرمول برای هر احتمال دو عاملی تک خروجی استفاده کنیم. می‌توانیم ورودی را به‌عنوان یک بردار و در نتیجه عملکرد کلی نورون‌ها به‌عنوان تبدیل یک بردار به یک مقدار اسکالر در نظر بگیریم.

مرحله پیش‌بینی شبکه مانند فهرست ۴ است.


public Double predict(Integer input1, Integer input2){
  return neurons.get(5).compute(
    neurons.get(4).compute(
      neurons.get(2).compute(input1, input2),
      neurons.get(1).compute(input1, input2)
    ),
    neurons.get(3).compute(
      neurons.get(1).compute(input1, input2),
      neurons.get(0).compute(input1, input2)
    )
  );
}

فهرست ۴ نشان می دهد که دو ورودی به سه نورون اول وارد می شوند، که خروجی آنها سپس به نورون های ۴ و ۵ هدایت می شود، که به نوبه خود به نورون خروجی تغذیه می شود. این فرآیند به عنوان feedforward شناخته می‌شود.

اکنون، می‌توانیم از شبکه بخواهیم که پیش‌بینی کند، همانطور که در فهرست ۵ نشان داده شده است.


Network network = new Network();
Double prediction = network.predict(Arrays.asList(115, 66));
System.out.println(“prediction: “ + prediction);

مطمئناً چیزی به دست می‌آوریم، اما این نتیجه وزن‌ها و سوگیری‌های تصادفی است. برای یک پیش‌بینی واقعی، ابتدا باید شبکه را آموزش دهیم.

شبکه را آموزش دهید

آموزش شبکه عصبی از فرآیندی به نام پشت انتشار پیروی می‌کند که در مقاله بعدی به طور عمیق‌تر آن را معرفی خواهم کرد. انتشار برگشتی اساساً تغییرات را از طریق شبکه به عقب می راند تا خروجی به سمت هدف مورد نظر حرکت کند.

ما می‌توانیم با استفاده از تمایز تابع، انتشار پس‌زمینه را انجام دهیم، اما برای مثال، می‌خواهیم کار متفاوتی انجام دهیم. ما به هر نورون ظرفیت "جهش" می دهیم. در هر دور تمرین (معروف به دوره)، یک نورون متفاوت را انتخاب می کنیم تا یک تنظیم کوچک و تصادفی برای یکی از ویژگی های آن (weight1، weight2) ایجاد کنیم. ، یا bias) و سپس بررسی کنید که آیا نتایج بهبود یافته است یا خیر. اگر نتایج بهبود یافت، این تغییر را با روش remember() حفظ خواهیم کرد. اگر نتایج بدتر شد، با روش forget() از تغییر صرف نظر خواهیم کرد.

برای پیگیری تغییرات، اعضای کلاس (نسخه‌های قدیم* وزن‌ها و سوگیری) را اضافه می‌کنیم. می‌توانید روش‌های mutate()، remember() و forget() را در فهرست ۶ مشاهده کنید.


public class Neuron() {
  private Double oldBias = random.nextDouble(-1, 1), bias = random.nextDouble(-1, 1); 
 public Double oldWeight1 = random.nextDouble(-1, 1), weight1 = random.nextDouble(-1, 1); 
 private Double oldWeight2 = random.nextDouble(-1, 1), weight2 = random.nextDouble(-1, 1);
public void mutate(){
      int propertyToChange = random.nextInt(0, 3);
      Double changeFactor = random.nextDouble(-1, 1);
      if (propertyToChange == 0){ 
        this.bias += changeFactor; 
      } else if (propertyToChange == 1){ 
        this.weight1 += changeFactor; 
      } else { 
        this.weight2 += changeFactor; 
      };
    }
    public void forget(){
      bias = oldBias;
      weight1 = oldWeight1;
      weight2 = oldWeight2;
    }
    public void remember(){
      oldBias = bias;
      oldWeight1 = weight1;
      oldWeight2 = weight2;
    }
}

خیلی ساده: متد mutate() یک ویژگی را به طور تصادفی و مقداری بین -۱ و ۱ را به طور تصادفی انتخاب می کند و سپس ویژگی را تغییر می دهد. متد forget() به مقدار قبلی برمی گردد. روش remember() مقدار جدید را در بافر کپی می کند.

اکنون، برای استفاده از قابلیت‌های جدید Neuron، روش train() را به Network اضافه می‌کنیم، همانطور که در شکل نشان داده شده است. فهرست ۷.


public void train(List<List<Integer>> data, List<Double> answers){
  Double bestEpochLoss = null;
  for (int epoch = 0; epoch < 1000; epoch++){
    // adapt neuron
    Neuron epochNeuron = neurons.get(epoch % 6);
    epochNeuron.mutate(this.learnFactor);

    List<Double> predictions = new ArrayList<Double>();
    for (int i = 0; i < data.size(); i++){
      predictions.add(i, this.predict(data.get(i).get(0), data.get(i).get(1)));
    }
    Double thisEpochLoss = Util.meanSquareLoss(answers, predictions);

    if (bestEpochLoss == null){
      bestEpochLoss = thisEpochLoss;
        epochNeuron.remember();
      } else {
    if (thisEpochLoss < bestEpochLoss){
      bestEpochLoss = thisEpochLoss;
      epochNeuron.remember();
    } else {
      epochNeuron.forget();
    }
  }
}

روش train() هزار بار روی data و answers List در آرگومان تکرار می‌شود. . اینها مجموعه های آموزشی با همان اندازه هستند. data مقادیر ورودی را نگه می‌دارد و answers پاسخ‌های شناخته شده و خوب خود را نگه می‌دارد. سپس روش بر روی آنها تکرار می شود و مقداری برای اینکه شبکه چقدر نتیجه را در مقایسه با پاسخ های شناخته شده و صحیح حدس زده است، دریافت می کند. سپس، یک نورون تصادفی را جهش می‌دهد و اگر آزمایش جدید نشان دهد که پیش‌بینی بهتری بوده است، تغییر را حفظ می‌کند.

نتایج را بررسی کنید

می‌توانیم نتایج را با استفاده از میانگین مربعات خطا (MSE) بررسی کنیم. فرمول، یک روش معمول برای آزمایش مجموعه ای از نتایج در یک شبکه عصبی. می‌توانید عملکرد MSE ما را در فهرست ۸ ببینید.


public static Double meanSquareLoss(List<Double> correctAnswers,   List<Double> predictedAnswers){
  double sumSquare = 0;
  for (int i = 0; i < correctAnswers.size(); i++){
    double error = correctAnswers.get(i) - predictedAnswers.get(i);
    sumSquare += (error * error);
  }
  return sumSquare / (correctAnswers.size());
}

تنظیم دقیق سیستم

اکنون تنها چیزی که باقی می‌ماند این است که داده‌های آموزشی را در شبکه قرار داده و آن را با پیش‌بینی‌های بیشتر امتحان کنید. فهرست ۹ نشان می دهد که چگونه داده های آموزشی را ارائه می دهیم.


List<List<Integer>> data = new ArrayList<List<Integer>>();
data.add(Arrays.asList(115, 66));
data.add(Arrays.asList(175, 78));
data.add(Arrays.asList(205, 72));
data.add(Arrays.asList(120, 67));
List<Double> answers = Arrays.asList(1.0,0.0,0.0,1.0);  

Network network = new Network();
network.train(data, answers);

در فهرست ۹ داده‌های آموزشی ما فهرستی از مجموعه‌های اعداد صحیح دو بعدی است (می‌توانیم آنها را وزن و قد در نظر بگیریم) و سپس لیستی از پاسخ‌ها (با ۱.۰ زن و ۰.۰ مرد).

اگر مقداری لاگ را به الگوریتم آموزشی اضافه کنیم، اجرای آن خروجی مشابه فهرست ۱۰ خواهد داشت.


// Logging:
if (epoch % 10 == 0) System.out.println(String.format("Epoch: %s | bestEpochLoss: %.15f | thisEpochLoss: %.15f", epoch, bestEpochLoss, thisEpochLoss));

// output:
Epoch: 910 | bestEpochLoss: 0.034404863820424 | thisEpochLoss: 0.034437939546120
Epoch: 920 | bestEpochLoss: 0.033875954196897 | thisEpochLoss: 0.431451026477016
Epoch: 930 | bestEpochLoss: 0.032509260025490 | thisEpochLoss: 0.032509260025490
Epoch: 940 | bestEpochLoss: 0.003092720117159 | thisEpochLoss: 0.003098025397281
Epoch: 950 | bestEpochLoss: 0.002990128276146 | thisEpochLoss: 0.431062364628853
Epoch: 960 | bestEpochLoss: 0.001651762688346 | thisEpochLoss: 0.001651762688346
Epoch: 970 | bestEpochLoss: 0.001637709485751 | thisEpochLoss: 0.001636810460399
Epoch: 980 | bestEpochLoss: 0.001083365453009 | thisEpochLoss: 0.391527869500699
Epoch: 990 | bestEpochLoss: 0.001078338540452 | thisEpochLoss: 0.001078338540452

فهرست ۱۰ زیان (واگرایی خطا از دقیقاً سمت راست) را نشان می دهد که به آرامی در حال کاهش است. یعنی به پیش بینی های دقیق نزدیک تر می شود. تنها چیزی که باقی می ماند این است که ببینیم مدل ما با داده های واقعی چقدر خوب پیش بینی می کند، همانطور که در فهرست ۱۱ نشان داده شده است.


System.out.println("");
System.out.println(String.format("  male, 167, 73: %.10f", network.predict(167, 73)));
System.out.println(String.format("female, 105, 67: %.10", network.predict(105, 67))); 
System.out.println(String.format("female, 120, 72: %.10f | network1000: %.10f", network.predict(120, 72))); 
System.out.println(String.format("  male, 143, 67: %.10f | network1000: %.10f", network.predict(143, 67)));
System.out.println(String.format(" male', 130, 66: %.10f | network: %.10f", network.predict(130, 66)));

در فهرست ۱۱، شبکه آموزش‌دیده خود را می‌گیریم و مقداری داده به آن می‌دهیم و پیش‌بینی‌ها را خروجی می‌دهیم. چیزی شبیه لیست ۱۲ دریافت می کنیم.


  male, 167, 73: 0.0279697143 
female, 105, 67: 0.9075809407 
female, 120, 72: 0.9075808235 
  male, 143, 67: 0.0305401413
  male, 130, 66: network: 0.9009811922

در فهرست ۱۲، می‌بینیم که شبکه با اکثر جفت‌های ارزش (aka بردارها) کار بسیار خوبی انجام داده است. این به مجموعه داده های زن تخمینی در حدود ۰.۹۰۷ می دهد که بسیار نزدیک به یک است. دو مرد ۰.۰۲۷ و ۰.۰۳۰ را نشان می‌دهند—نزدیک به ۰. مجموعه داده‌های مرد پرت (۱۳۰، ۶۷) احتمالاً زن هستند، اما با اطمینان کمتر در ۰.۹۰۰.

نتیجه گیری

چند راه برای تنظیم شماره گیری در این سیستم وجود دارد. برای یک، تعداد دوره های آموزشی یک عامل اصلی است. هر چه دوره‌ها بیشتر باشد، مدل با داده‌ها هماهنگ‌تر می‌شود. اجرای دوره‌های بیشتر می‌تواند دقت داده‌های زنده را که با مجموعه‌های آموزشی مطابقت دارد، بهبود بخشد، اما همچنین می‌تواند منجر به تمرین بیش از حد شود. یعنی مدلی که با اطمینان نتایج اشتباه را برای موارد لبه پیش بینی می کند.

از مخزن GitHub من برای کد کامل این آموزش به همراه مقداری اضافی دیدن کنید زنگ ها و سوت ها.