Java - algoritmy aproximácie

Z Kiwiki
Skočit na navigaci Skočit na vyhledávání

Klasickým problémom vo viacerých vedných disciplínach je nájsť trend vývoja určitej situácie, pričom poznáme len diskrétne hodnoty. Úloha je prostá nájsť takú krivku, ktorá sa najviac približuje daným diskrétnym hodnotám.

Metóda najmenších štvorcov

Túto úlohu riešia algoritmy aproximácie. Medzi najpoužívanejší algoritmus patrí metóda najmenších štvorcov[1]. Jedná sa o optimalizačný problém, kde je treba nájsť takú krivku (modrá čiara) od ktorej je štvorec vzdialenosti vstupných bodov (červené body) minimálny.

Metóda najmenších štvorcov

V praktickom príklade si ukážeme výpočet lineárnej, logaritmickej, exponenciálnej a mocninovej aproximácie. Teória, ktorú budeme potrebovať je v časti Algoritmy numerickej aproximácie (riešené príklady).

Pre úplnosť poznamenajme, že máme k dispozícii n bodov v rovine a cez tieto body chceme preložiť nejakú krivku. Uvedieme len základné tvary hľadaných aproximácií a matematické vyjadrenie riešenia problému.

Lineárna aproximácia

Tvar rovnice hľadanej krivky: [math]y=ax+b\,[/math]

Riešenie:

[math]\begin{align} & a=\frac{n\sum\limits_{i=1}^{n}{{{x}_{i}}{{y}_{i}}}-\sum\limits_{i=1}^{n}{{{x}_{i}}}\sum\limits_{i=1}^{n}{{{y}_{i}}}}{n\sum\limits_{i=1}^{n}{x_{i}^{2}}-{{\left( \sum\limits_{i=1}^{n}{{{x}_{i}}} \right)}^{2}}} \\ & b=\frac{\sum\limits_{i=1}^{n}{{{y}_{i}}}-a\sum\limits_{i=1}^{n}{{{x}_{i}}}}{n} \\ \end{align}[/math]

Logaritmická aproximácia

Tvar rovnice hľadanej krivky: [math]y=a\ln x+b\,[/math]

Riešenie:

[math]\begin{align} & a=\frac{n\sum\limits_{i=1}^{n}{\left( \ln {{x}_{i}} \right){{y}_{i}}}-\sum\limits_{i=1}^{n}{\ln {{x}_{i}}}\sum\limits_{i=1}^{n}{{{y}_{i}}}}{n\sum\limits_{i=1}^{n}{{{\left( \ln {{x}_{i}} \right)}^{2}}}-{{\left( \sum\limits_{i=1}^{n}{\ln {{x}_{i}}} \right)}^{2}}} \\ & b=\frac{\sum\limits_{i=1}^{n}{{{y}_{i}}}-a\sum\limits_{i=1}^{n}{\ln {{x}_{i}}}}{n} \\ \end{align}[/math]

Exponenciálna aproximácia

Tvar rovnice hľadanej krivky: [math]y=b{{e}^{ax}}\,[/math]

Riešenie:

[math]\begin{align} & a=\frac{n\sum\limits_{i=1}^{n}{{{x}_{i}}\ln {{y}_{i}}}-\sum\limits_{i=1}^{n}{{{x}_{i}}}\sum\limits_{i=1}^{n}{\ln {{y}_{i}}}}{n\sum\limits_{i=1}^{n}{x_{i}^{2}}-{{\left( \sum\limits_{i=1}^{n}{{{x}_{i}}} \right)}^{2}}} \\ & B=\frac{\sum\limits_{i=1}^{n}{\ln {{y}_{i}}}-a\sum\limits_{i=1}^{n}{{{x}_{i}}}}{n} \\ \end{align}[/math]

kde [math]B=\ln b[/math].

Mocninová aproximácia

Tvar rovnice hľadanej krivky: [math]y=b{{x}^{a}}\,[/math]

Riešenie:

[math]\begin{align} & a=\frac{n\sum\limits_{i=1}^{n}{\ln {{x}_{i}}\ln {{y}_{i}}}-\sum\limits_{i=1}^{n}{\ln {{x}_{i}}}\sum\limits_{i=1}^{n}{\ln {{y}_{i}}}}{n\sum\limits_{i=1}^{n}{{{\left( \ln {{x}_{i}} \right)}^{2}}}-{{\left( \sum\limits_{i=1}^{n}{\ln {{x}_{i}}} \right)}^{2}}} \\ & B=\frac{\sum\limits_{i=1}^{n}{\ln {{y}_{i}}}-a\sum\limits_{i=1}^{n}{\ln {{x}_{i}}}}{n} \\ \end{align}[/math]

kde [math]B=\ln b[/math].

Riešenie v Jave

Na začiatku bolo povedané že musíme mať niekoľko (min. 2) body podľa ktorých budeme aproximovať. Ako prvý krok si vytvoríme jednoduchú triedu Bod, ktorá bude reprezentovať jeden bod v 2D rovine:

public class Bod {
    public double x,y;

    public Bod(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public Bod() {
        this(0,0);
    }
}

Trieda obsahuje len konštruktor. Ostatné metódy nie sú potrebné, keďže vnútorné premenné x a y sú verejné.

Vytvorme si novú triedu a nazvime ju Extrapolacia. V tejto triede budeme riešiť algoritmy aproximácie a interpolácie. Naša trieda Extrapolacia musí obsahovať n bodov, ktoré použijeme pri výpočte parametrov aproximovanej krivky. Počet týchto bodov nie je dopredu známy a dokonca sa môže počet behu programu meniť. Pre ukladanie týchto bodov použijeme vstavané rozhranie List[2] (zoznam), resp. triedu ArrayList[3]. Trieda ArrayList dovoľuje vytvoriť zoznam, resp. pole z ľubovoľných objektov. V našom prípade potrebujeme vytvoriť zoznam bodov:

   List body<Bod>=new ArrayList<Bod>()

Trieda Extrapolacia bude okrem objektu body ešte obsahovať informáciu type aproximácie, ktorá sa bude počítať (typAproximacie) a parametroch a, b (aproxA, aproxB), ktorá vlastne charakterizujú danú krivku.

import java.util.ArrayList;
import java.util.List;

public class Extrapolacia {

    public final static int aproxLin = 0;
    public final static int aproxLog = 1;
    public final static int aproxExp = 2;
    public final static int aproxMoc = 3;
    private int typAproximacie;
    private double aproxA, aproxB;
    public List<Bod> body;

    public Extrapolacia() {
        body = new ArrayList<Bod>();
        this.typAproximacie = Extrapolacia.aproxLin;
    }

    public void addBod(Bod X) {
        body.add(X);
    }

    public double getA() {
        return aproxA;
    }

    public double getB() {
        return aproxB;
    }

    public int getTypAproximacie() {
        return typAproximacie;
    }  
  // ostatne metody triedy Extrapolacia
}

Pre jednoduchšie určovanie typu aproximácie sme si vytvorili statické konštanty aproxLin, aproxLog, aproxExp a aproxMoc. Pri vytváraní objektu Extrapolacia (v konštruktore) sa určí ako prednastavený typ aproximácie lineárna aproximácia.

Metóda addBod pridá do zoznamu bodov body nový bod.

Implementácie metódy najmenších štvorcov

Prvým krokom pri implementácii metódy najmenších štvorcov je vypočítať súčty (sumy) uvedené vo vzorcoch.

Pri lineárnej aproximácie máme súčet x-ových súradníc, súčet y-ových súradníc, súčet štvorca x-vých súradníc a výrazov [math]x_i * y_i[/math]. V programe si označme tieto sumy premennými sx, sy, sxx a sxy.

Pri logaritmickej aproximácii máme naviac súčet logaritmu x-vých súradníc, súčet štvorca logaritmu x-vých súradníc a súčet výrazov [math]ln x_i * y_i[/math]. Označme si tieto sumy premennými slx, slxx a slxy.

Podobne si nazveme aj ostatné sumy. Ak máme všetky tieto sumy vypočítané, môžeme ch dosadiť do výsledného vzorca. Vzorec do ktorého dosadíme tieto sumy bude určený podľa požadovaného typu aproximácie.

Triedu Aproximator doplníme o nasledujúcu metódu:

    public void aproximacia(final int typ) {
        this.typAproximacie = typ;
        double sx = 0, sxx = 0, sy = 0, sxy = 0, slxly = 0;
        double slx = 0, slxx = 0, sly = 0, slxy = 0, sxly = 0;
        
        // vypocet sum do vzorcov
        for (int i = 0; i < this.body.size(); i++) {
            sx += this.body.get(i).x;
            slx += Math.log(this.body.get(i).x);
            sy += this.body.get(i).y;
            sly += Math.log(this.body.get(i).y);
            sxy += this.body.get(i).x * this.body.get(i).y;
            slxy += Math.log(this.body.get(i).x) * this.body.get(i).y;
            sxly += this.body.get(i).x * Math.log(this.body.get(i).y);
            slxly += Math.log(this.body.get(i).x) * Math.log(this.body.get(i).y);
            sxx += this.body.get(i).x * this.body.get(i).x;
            slxx += Math.log(this.body.get(i).x) * Math.log(this.body.get(i).x);
        }
        
        int n = this.body.size();
        // vysledny vzoroc urcime podla pozadovaneho typu aproximacie
        switch (typ) {
            case Extrapolacia.aproxLin:
                this.aproxA = (n * sxy - sy * sx) / (n * sxx - sx * sx);
                this.aproxB = (sy - this.aproxA * sx) / n;
                break;
            case Extrapolacia.aproxLog:
                this.aproxA = (n * slxy - sy * slx) / (n * slxx - slx * slx);
                this.aproxB = (sy - this.aproxA * slx) / n;
                break;
            case Extrapolacia.aproxExp:
                this.aproxA = (n * sxly - sly * sx) / (n * sxx - sx * sx);
                this.aproxB = (sly - this.aproxA * sx) / n;
                this.aproxB = Math.exp(this.aproxB);
                break;
            case Extrapolacia.aproxMoc:
                this.aproxA = (n * slxly - sly * slx) / (n * slxx - slx * slx);
                this.aproxB = (sly - this.aproxA * slx) / n;
                this.aproxB = Math.exp(this.aproxB);
                break;
        }
    }

V predchádzajúcej metóde boli vypočítané len koeficienty a, b. Do našej aplikácie potrebujeme doplniť ďalšiu metódu, ktorá nám vypočíta funkčnú hodnotu z danej aproximovanej krivky. Do triedy Aproximator doplníme metódu:

    public double aproximovanaHodnota(double x) {
        double y = 0;
        switch (this.typAproximacie) {
            case Extrapolacia.aproxLin:
                y = this.aproxA * x + this.aproxB;
                break;
            case Extrapolacia.aproxLog:
                y = this.aproxA * Math.log(x) + this.aproxB;
                break;
            case Extrapolacia.aproxExp:
                y = this.aproxB * Math.exp(this.aproxA * x);
                break;
            case Extrapolacia.aproxMoc:
                y = this.aproxB * Math.pow(x, this.aproxA);
                break;
        }
        return y;
    }

Použitie metód aproximácie v aplikácii

Vo vzorovej aplikácii pridáme na kartu 'Extrapolácia' komponenty podľa nasledujúceho obrázku:

Vizuálny návrh časti aplickácie - aproximácia

Opis komponentov:

textField

  • Dva komponenty textfielt slúžia na zadávanie body (x, y súradnica) do aplikácie
    • názov komponentu: textExtraX, textExtraY

label

  • Zobrazie počtu bodov pre aproximáciu (na obrázku má hodnotu 0)
    • názov komponentu: labelInterpolaciaBody
  • Zobrazenie výslednej rovnice proximácie
    • názov komponentu: labelRovnica

comboBox

  • Výber typu aproximácie - rozbaľovací zoznam
    • názov komponentu: comboAproximacia
    • Obsahuje 4 položky: Lineárna, Logaritmicá, Exponenciálna a Mocninová.
      • Tieto položky nastavíme pomocou vlastnosti model.

button

  • Pridanie bodu do zoznamu bodov pre aproximáciu
    • text komponentu: 'Pridaj bod'
    • názov komponentu: tlcPridajBod
    • udalosť tlačidla: tlcPridajBodActionPerformed
  • Spustenie aproximácie a vykreslenie výsledku
    • text komponentu: 'Aproximuj!'
    • názov komponentu: tlcAproximuj
    • udalosť tlačidla: tlcAproximujActionPerformed


Obsluha kliknutia tlačidiel:

Udalosť Pridaj bod

    private void tlcPridajBodActionPerformed(java.awt.event.ActionEvent evt) {                                             
        double x, y;
        x = Double.valueOf(textExtraX.getText());
        y = Double.valueOf(textExtraY.getText());
        e.addBod(new Bod(x, y));
        labelInterpolaciaBody.setText(String.valueOf(e.body.size()));
    }

Udalosť Aproximuj!

    private void tlcAproximujActionPerformed(java.awt.event.ActionEvent evt) {                                              

        int i = comboAproximacia.getSelectedIndex();
        e.aproximacia(i);
        NumberFormat formatter = new DecimalFormat("#0.000");
        double a, b;
        a = e.getA();
        b = e.getB();
        // zobrazenie rovnice aproximacie
        String rovnica = "y=";
        switch (e.getTypAproximacie()) {
            case Extrapolacia.aproxLin:
                rovnica += formatter.format(a) + "x +" + formatter.format(b);
                break;
            case Extrapolacia.aproxLog:
                rovnica += formatter.format(a) + "ln x +" + formatter.format(b);
                break;
            case Extrapolacia.aproxExp:
                rovnica += formatter.format(b) + "e^(" + formatter.format(a) + "x)";
                break;
            case Extrapolacia.aproxMoc:
                rovnica += formatter.format(b) + "x^" + formatter.format(a);
                break;
        }
        labelRovnica.setText(rovnica); // label s rovnicou aproximacie

        //vykreslenie aproximovanej krivky        
        int sirka, vyska;
        sirka = panel.getWidth();
        vyska = panel.getHeight();
        t.setSize(sirka, vyska);

        g = panel.getGraphics();
        g.clearRect(1, 1, sirka - 2, vyska - 2);
        g.setColor(Color.GRAY);
        g.drawLine(0, vyska / 2, sirka, vyska / 2);
        g.drawLine(sirka / 2, 0, sirka / 2, vyska);

        g.setColor(Color.GREEN);
        double x1, y1, x2, y2 = 0;
        x1 = -t.getStrana();
        y1 = e.aproximovanaHodnota(x1);
        for (x2 = -t.getStrana(); x2 < t.getStrana(); x2 += t.getKrok()) {
            y2 = e.aproximovanaHodnota(x2);
            g.drawLine(t.getX(x1), t.getY(y1), t.getX(x2), t.getY(y2));
            x1 = x2;
            y1 = y2;
        }

        // zobrazenie bodov, pre ktore pocitame aproximaciu
        int r = 3;
        g.setColor(Color.BLACK);
        for (Bod A : this.e.body) {
            g.drawOval(t.getX(A.x) - r, t.getY(A.y) - r, 2 * r, 2 * r);
        }
    }

Výsledok:

Aproximácia - riešenie

Zdroje a odkazy