Gleitender Mittelwert -> Algorithmus

OP #7547751
Lesenswert?

Hallo zusammen, ich brauch für die SPS einen Moving-Average-Filter/Gleitenden Mittelwert. Das ist ja eigentlich auch leicht und effizient zu programmieren, indem man dem Durchschnitt jeweils den neu hinzukommenden Wert anteilig addiert und den herausfallenden Wert anteilig subtrahiert. Jetzt frag ich mich, ob man dabei einen ungewollten Drift reinbekommen kann, wenn man mit Gleitkommazahlen arbeitet, weil es beim addieren und subtrahieren des gleichen Wertes evtl. zu Rundungen bzw. ungleichen Rundungsfehlern kommt.

Kann jemand was dazu sagen? Ist das ein reales Problem oder gleicht sich das statistisch sowieso aus? Oder muss man die Zahlen auf bestimmte Weise wählen oder Vorrunden um nicht in den Bereich der letzten signifikanten Bits zu kommen?

Danke!

1
  Average:= Average + (aInput - ValueTable[CurrentIndex])/length(ValueTable);
2
  ValueTable[CurrentIndex]:= aInput;
3

4
  CurrentIndex:= CurrentIndex + 1;
5
  if CurrentIndex > high(ValueTable) then CurrentIndex:= 0;
OP #7547760
Lesenswert?

N. M. schrieb:

Aber wenn du beim reinstecken in den Filter den gleichen Fehler machst wie beim rausholen...

Das ist ja gerade die Frage, ob der Fehler gleich bleibt.

Ein Extrembeispiel wäre, wenn der Durchschnittswert beim "Reinstecken" des Wertes so groß ist, dass die kleine zugefügte Zahl komplett verschwindet. Also dass das kleinste Bit der Mantisse wertmäßig noch größer ist als der ganze zugefügte Wert. Zum Zeitpunkt, wo der Wert wieder aus dem Mittelwert fällt, könnte der Durchschnitt klein genug sein, dass sich der Wert doch wieder abbildet. Dann hätte man zwischen Reinstecken und Rausholen einen negativen Drift.

#7547828
Lesenswert?

Das habe ich mich auch schon gefragt kann Dir aber dazu auch keine definitive Antwort geben. Ich vermute aber auch, dass das unter ungünstigen Umständen wegdriften könnte.

Aber falls die Eingabedaten eigentlich Ganzzahlen sind, z.B. wenn sie von einen ADC kommen, oder sich darauf abbilden lassen, mache ich das immer so, dass ich im Ringpuffer die Integerwerte speichere und damit nicht den Mittelwert sondern die Summe über die Fenstergröße berechne, und erst hinterher durch die Länge teile. Da dann nur Integerwerte addiert und subtrahiert werden, gibt es keine Rundungsfehler. Der Wertebereich von "Sum" muss natürlich groß genug sein.

Analog zu Deinem Beispiel:

1
  Sum := Sum + aInput - ValueTable[CurrentIndex];  // hier alles Integer
2
  ValueTable[CurrentIndex]:= aInput;
3
  Average := Sum / length(ValueTable);             // erst hier Float
4
  ...
Gast #7547836
Lesenswert?

Hallo Maxe,

also so ein Filter habe ich auch schon in einer SPS (Siemens S7/300) realisiert. Für meine Zwecke habe ich dabei einfach jeweils von n vorausgegangenen Messwerten den Mittelwert berechnet. Mit jedem neu hinzugekommenen Messwert Bin ich dann einen Messwert weiter gerückt und habe wieder von den letzten n Werten den Mittelwert berechnet. Problem dabei ist lediglich, dass man erst ab mindestens n verfügbaren Messwerten einen "echten" Wert bekommt.

Hat zumindest für meine Anwendung (Messkurven glätten) hervorragend funktioniert.

Ob das jetzt genau der Definition von "gleitendem Mittelwertfilter" entspricht, weiß ich nicht. Wie gesagt, es hat einwandfrei funktioniert, und irgendwelche Probleme mit Rechenungenauigkeiten, Auslöschungen etc. gab es dabei nicht.

ciao

Marci

OP #7547844
Lesenswert?

Olly T. schrieb:

Aber falls die Eingabedaten eigentlich Ganzzahlen sind, z.B. wenn sie von einen ADC kommen, oder sich darauf abbilden lassen, mache ich das immer so, dass ich im Ringpuffer die Integerwerte speichere und damit nicht den Mittelwert sondern die Summe über die Fenstergröße berechne, und erst hinterher durch die Länge teile. Da dann nur Integerwerte addiert und subtrahiert werden, gibt es keine Rundungsfehler. Der Wertebereich von "Sum" muss natürlich groß genug sein.

Das wäre natürlich eine Möglichkeit. Ist auch gut zu wissen, dass nicht nur ich die Befürchtung habe und es die Problematik vielleicht tatsächlich gibt. In meinem Fall gehen berechnete Werte in den Filter, da würde ich gerne bei Gleitkommazahlen (als Eingang) bleiben. Meine letzte Überlegung war, dem Filter einen einstellbaren Eingangsbereich (Definitionsbereich) zu verpassen und dann diesen Bereich geteilt durch die Anzahl Stützpunkte auf eine 32Bit-Ganzzahl zu projizieren, also skalieren. Die Summe dann als 64Bit führen und beim Auslesen auf die Gleitkommazahl zurückskalieren. Insgesamt halt recht aufwendig.

Marci W. schrieb:

Für meine Zwecke habe ich dabei einfach jeweils von n vorausgegangenen Messwerten den Mittelwert berechnet. Mit jedem neu hinzugekommenen Messwert Bin ich dann einen Messwert weiter gerückt und habe wieder von den letzten n Werten den Mittelwert berechnet. [...] Ob das jetzt genau der Definition von "gleitendem Mittelwertfilter" entspricht, weiß ich nicht.

Doch schon, genau so ist der gleitende Mittelwert ja definiert. Der Algorithmus mit dem Hinzufügen und Herausnehmen ist eher eine Optimierung in Bezug auf die Rechenzeit. Wenn ich einen gleitenden Mittelwert mit 4000 Stützpunkten habe, müsste ich die ja alle in einem SPS-Zyklus aufaddieren. Mit der relativen Berechnung sind es dagegen nur 3 Rechenoperationen. Mag sein, dass auch die Tausende Rechnoperationen kein Problem für die SPS sind, aber ich will mir da auch keinen künstlichen Flaschenhals schaffen.

Problem dabei ist lediglich, dass man erst ab mindestens n verfügbaren Messwerten einen "echten" Wert bekommt.

Das ist ja prinzipbedingt so. Man geht da einfach von einem Anfangswert aus, initialisiert also das ganze Array mit einem sinnvollen Anfangswert.

Persönliche Seite #7547847
Lesenswert?

Das so anzusetzen, ist gelinde gesagt "Murks"!

Man schleppt nämlich zusätzlich zum eigentlichen Wert noch das Integral des Rundungsfehlers mit und das ist nicht automatisch statistisch Null, sondern es ergibt sich ein beliebig tief gehendes Spektrum.

Richtig ist, vollständig zu addieren und jeweils neu zu dividieren. Dann gibt es nur einen temporären Rauschfehler, der erheblich kleiner ist und spektral auf den Bereich der betrachteten Punkte begrenzt ist.

Gast #7547853
Lesenswert?

Maxe schrieb:

Mittelwert mit 4000 Stützpunkten

Ups, das ist natürlich ne Menge. Es würde mich interessieren, wo du derart viele Werte berücksichtigen musst.

Beim Vorrücken des Index den "rausfallenden" Wert am Anfang des Fensters von der Summe subtrahieren und den neu hinzugekommenen Wert addieren (ähnlich wie Deine Idee). Dann wieder teilen. Dann wäre die Rechnerei effizienter. Allerdings nur bei Ganzzahlen. Sonst hast Du wieder Rechenfehler, die sich aufsummieren können (da bin ich jetzt aber zu faul zu überlegen, ob das auch zutrifft).

Aber wie gesagt, mir kommt eine derart große Anzahl von Werten komisch vor.

SPS-mäßig könntest Du die Funktion in einen eigenen Baustein packen und diesem dann, wenn neue Messwerte ankommen, ein Startsignal beim Aufruf mitgeben. Im Baustein dann in jedem Zyklus nur eine kleinere Wertemenge addieren. Kommt halt alles sehr auf Deine Randbedingungen an (wie schnell kommen die Messwerte rein, wie schnell brauchst du den gleitenden Mittelwert, wie schnell ist Deine SPS, was hat Sie sonst noch zu tun, max. zulässige Zykluszeit etc. pp.), das übliche halt.

ciao

Marci

OP #7547856
Lesenswert?

J. S. schrieb:

Man schleppt nämlich zusätzlich zum eigentlichen Wert noch das Integral des Rundungsfehlers mit und das ist nicht automatisch statistisch Null, sondern es ergibt sich ein beliebig tief gehendes Spektrum.

Ok, dann sind wir schon zu dritt, die ein Problem in den Gleitkommazahlen sehen. Arbeitet man mit Ganzzahlen integrieren sich die Rundungsfehler ja nicht auf, sondern "fallen" wieder raus, wenn der Wert abgezogen wird. Man hat dann als Rundungsfehler nur noch die Summe der Rundungen im Ringspeicher. Ich würde vermuten, dass die sehr klein sind, man täuscht sich aber leicht.

Also, bei 4000 Stützstellen kann im schlechtesten Fall jeweils das letzte Bit falsch sein, also summiert ein Wert von 4000 oder 12 Bit. Von 32 Bit bleiben dann noch 20 Bit verteilt auf den Eingangsbereich. Dürfte gut genug sein.

OP #7547858
Lesenswert?

Marci W. schrieb:

Es würde mich interessieren, wo du derart viele Werte berücksichtigen musst.

Eine Regelung in der Verfahrenstechnik, das Fluid ist 3 Minuten in der Leitung unterwegs, also 180 Sek. Bei 10 Zyklen pro Sekunde hat man 1800 Werte. Die Zyklenzeit könnte man wohl verlängern, und es gäbe wohl auch kaskadische Algorithmen für den gleitenden Mittelwert. Muss man aber auch alles wieder durchdenken.

#7547860
Lesenswert?

Wenn du das mit Integerwerten machst und die Summe der Werte speicherst anstatt dem Mittelwert und den Mittelwert erst für die Ausgabe verwendest, dann solltest du keine Rundungsfehler haben. Die summenvariable muss natürlich groß genug sein um den Wert auch aufnehmen zu können.

In etwa so

1
int32_t sum = 0; // muss so groß sein, dass 'count * MAX(next) reinpasst'
2
const int32_t count = 4;
3

4
int32_t calcMediumValue(int32_t next) {
5
 sum = sum - (sum / count) + next;
6
 return sum / count;
7
}

Wahrscheinlich ist es dasselbe was J. S. vorgeschlagen hat.

Übersehe ich was?

Michael

#7547886
Lesenswert?

Für das Aufsummieren von Floating Point Werten gibt es den Kahan Algorithmus. Der ist wesentlich genauer weil er sich neben der Summe auch den laufenden Rundungsfehler merkt.

https://en.wikipedia.org/wiki/Kahan_summation_algorithm

Lässt sich 1:1 für den gleitenden Mittelwert einsetzen. Die Subtraktion ersetzt Du einfach mit einer Negation und Addition.

Drift wird es immer noch geben, aber wesentlich weniger.

Was Du machen könntest wäre ständig eine zweite Instanz des Filters aufzubauen. Sobald die 4000 Samples erreicht sind, verwifst Du den aktiven Filter und schaltest auf den frischen um.

So kann nie mehr als die Drift von 4000 Samples ins System kommen weil Du immer neu ansetzt. Verdoppelt natürlich die Rechenzeit.

In Verbindung mit Kahan wird dieses Verfahren vermutlich sogar genauer sein, als in jedem Zyklus ganz naiv alle Werte zu addieren.

Wenn Du es auf die Spitze treiben willst, dann trenne zusätzlich noch die Additionen und Subtraktionen und berechne die Differenz erst dann, wenn Du sie brauchst. So vermeidest Du auch das Problem des Genauigkeitverlustes durch Auslöschung in der Subtraktion:

https://de.wikipedia.org/wiki/Ausl%C3%B6schung_(numerische_Mathematik)

Gast #7547920
Lesenswert?

Wenn Du es auf die Spitze treiben willst, dann trenne zusätzlich noch die Additionen und Subtraktionen und berechne die Differenz erst dann, wenn Du sie brauchst. So vermeidest Du auch das Problem des Genauigkeitverlustes durch Auslöschung in der Subtraktion:

https://de.wikipedia.org/wiki/Ausl%C3%B6schung_(numerische_Mathematik)

Aber nicht vergessen, vorher die sogenannte "Optimierung" des Compilers aus zu schalten, weil diese u.U. jegliche Genauigkeitsverbesserung zunuchte macht. Steht im Gleichen Artikel weiter unten:

https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Possible_invalidation_by_compiler_optimization

"In principle, a sufficiently aggressive optimizing compiler could destroy the effectiveness of Kahan summation ... if the compiler simplified expressions according to the associativity rules of real arithmetic"

Der bekannte intel c++ compiler ist wohl so ein Stück schlitzohriger Software, der es in diesem Fall mit der Optimierung (Einsparung von Rechenzeit) übertreibt.

Aber ob der Compiler "aggresiv optimiert" sollte der obligatorische Regressiontest (Vergleich Ergebnisse alter, neuer Softwarestand) zeigen.

Gast #7547957
Lesenswert?

Was spricht dagegen, den hinzukommenden Wert (sagen wir 16-Bit) zum 32-Bit-Akkumulator zu addieren und in den Buffer zu kopieren, und den herausfallenden Wert vom Akkumulator zu subtrahieren. Und dann jeweils den Akkumulator durch die Länge des Filters zu teilen (sinnvollerweise als Bitshift, bei den Registergrößen machbar bis Filterlänge 2^15)?

Operationen: eine Addition, eine Subtraktion, beide ohne Drift, ein Speichern, ein Lesen, ein Bitshift (mit vorheriger Addition der halben Filterlänge zur korrekten Rundung)?

#7547966
Lesenswert?

Justin S. schrieb:

Was spricht dagegen, den hinzukommenden Wert (sagen wir 16-Bit) zum 32-Bit-Akkumulator zu addieren

Das ist genau das was ich oben gepostet habe, aber die Rundung habe ich tatsächlich vergessen. Mir ist noch nicht klar, ob ich die auch bei der Berechnung vom sum verwenden muss oder nur für das Ergebnis.

Ich habe für beide Variablen 32 bit Typen verwendet und eine Division anstatt Bitshift, da diese Optimierung Sache des Compilers ist wenn die Größe intelligent gewählt ist.

Hier mit Rundung:

1
int32_t sum = 0; // muss so groß sein, dass 'count * MAX(next) reinpasst'
2
const int32_t count = 4;
3
int32_t calcMediumValue(int32_t next) {
4
 sum = sum - ((sum + count/2) / count) + next; // is the '+count/2' for 5/10 rounding correct here?
5
 return (sum + count/2) / count; // '+count/2' for 5/10 rounding
6
}

Michael

#7547976
Lesenswert?

Maxe schrieb:

Man hat dann als Rundungsfehler nur noch die Summe der Rundungen im Ringspeicher. Ich würde vermuten, dass die sehr klein sind, man täuscht sich aber leicht.

Integer-haben beim aufsummieren wie hier eine höhere Auflösung als Fließpunkt . Es ist nur mehr Aufwand, Integer richtig zu skalieren (also Fixpunkt ohne Überlauf)

Also, bei 4000 Stützstellen kann im schlechtesten Fall jeweils das letzte Bit falsch sein, also summiert ein Wert von 4000 oder 12 Bit. Von 32 Bit bleiben dann noch 20 Bit verteilt auf den Eingangsbereich. Dürfte gut genug sein.

Bei float waren es nur 10 Bit. Bei Double natürlich mehr, aber wenn 32 Bit int als Summe nicht reichen, ist es einfach, noch 16 oder 32 dazu zu nehmen. Dann ist int genauso gut. Dann nimm aber 4096 Stützstellen, um die Division rein durch schieben zu machen.

#7547983
Lesenswert?

Michael D. schrieb:

Wenn du das mit Integerwerten machst und die Summe der Werte speicherst anstatt dem Mittelwert und den Mittelwert erst für die Ausgabe verwendest, dann solltest du keine Rundungsfehler haben.

Du hast zwei Integerdivisionen 'sum/count'. Nur in den seltenen Fällen, dass 'sum' ein ganzzahliges Vielfaches von 'count' ist, entsteht kein Rundungsfehler.

Gast #7547992
Lesenswert?

Rolf schrieb:

Du hast zwei Integerdivisionen 'sum/count'. Nur in den seltenen Fällen, dass 'sum' ein ganzzahliges Vielfaches von 'count' ist, entsteht kein Rundungsfehler.

Ich denke der Michael meint, dass sich Rundungsfehler nicht immer weiter aufaddieren und dadurch zu einem Drift führen (das war ja die ursprüngliche Frage).

Gast #7548013
Lesenswert?

Michael D. schrieb:

Das ist genau das was ich oben gepostet habe

Nein, Du programmierst da etwas völlig anderes. Ich meine folgendes (ziemlich das, was Olly T. anfangs andeutete):

1
int32_t sum = 0;
2
const int32_t count = 256;
3
int32_t calcMediumValue(int32_t next) {
4
    sum -= ringbuffer[idx];
5
    ringbuffer[idx] = next;
6
    sum += next;
7
    if ( --idx < 0 )
8
        idx = count - 1;
9
    return (sum + count/2) / count; // '+count/2' for 5/10 rounding
10
}
11

12
initRingbuffer(...)
#7548032
Lesenswert?

Steve van de Grens schrieb:

Rolf schrieb:

Du hast zwei Integerdivisionen 'sum/count'. Nur in den seltenen Fällen, dass 'sum' ein ganzzahliges Vielfaches von 'count' ist, entsteht kein Rundungsfehler.

Ich denke der Michael meint, dass sich Rundungsfehler nicht immer weiter aufaddieren und dadurch zu einem Drift führen.

Wenn der Wert des Divisionsergebnisses xxx,5 ist, dann rundet der von Compiler generierte Code entweder immer auf xxx oder immer auf xxx+1. Dieser Fehler summiert sich in einer Richtung auf, und so entsteht eine Drift.

(Firma: Wilhelm.Consulting) #7548074
Lesenswert?

FYI, die übliche Moving Average Implemenierungen sind eigentlich "falsch". (Zu lesen: irgendwo vereinfacht)

Siehe https://en.m.wikipedia.org/wiki/Moving_average#Cumulative_moving_average

Es geht vor allem um AVG=(n*alter_avg+neuer_wert)/(n+1).

Wenn der AVG driften würde (egal in welche Richtung), dann würde er durch diese Integration/summation zu einem stabilen wert konvergieren (weil ja n/(n+1) kleiner 1 ist).

Es gibt da zig Abwandlungen, wie man diese Verhältnismäßig komplexe Formel annähern kann. Sind irgendwo alles moving-average-filter aber mit Vor-/Nachteilen.

73

OP #7548165
Lesenswert?

Max H. schrieb:

Für das Aufsummieren von Floating Point Werten gibt es den Kahan Algorithmus. [...] Drift wird es immer noch geben, aber wesentlich weniger.

Danke, das werde ich mir auf jeden Fall merken. Allerdings möchte ich bei dem Filter wie es nur geht driftfrei sein. Weil es eben sehr schwer ist den tatsächlichen Drift einzuschätzen und sich letztlich der Regler nicht im Laufe der Zeit verändern darf. Es kommt letztlich nicht so auf eine hohe Genauigkleit an, sondern auf langfristige Zuverlässigkeit. Wenn der Filter im Laufe eines Jahres um ein paar Prozent abdriftet (k.A. ob realistisch), dann verändert sich auch das Reglerverhalten und das eher unvorhersehbar.

Was Du machen könntest wäre ständig eine zweite Instanz des Filters aufzubauen. Sobald die 4000 Samples erreicht sind, verwifst Du den aktiven Filter und schaltest auf den frischen um.

Ist auch keine schlechte Idee, einfach regelmäßig wieder nachziehen. Allerdings gibt das dann u.U. ungleiche Zykluszeiten, was auch wieder unvorhersehbare Effekte auslösen kann.

-- Justin S. schrieb im Beitrag #7547957:

Was spricht dagegen, den hinzukommenden Wert (sagen wir 16-Bit) zum 32-Bit-Akkumulator zu addieren und in den Buffer zu kopieren, [...]

Dagegen spricht, dass die Werte nicht als Ganzzahlen vorliegen, ansonsten ist das ja genau der Algorithmus, den ich verwende.

(sinnvollerweise als Bitshift, bei den Registergrößen machbar bis Filterlänge 2^15)?

Es geht hier um eine SPS, die hat eine FPU und unterstützt auch Divisionen. Und auch die absolute Genauigkeit ist beim Einsatz von Double bzw. 32bit Integern kein Problem. Es geht wirklich nur um einen Drift mit bleibender Abweichung.

-- Bruno V. schrieb im Beitrag #7547976:

Maxe schrieb:

Man hat dann als Rundungsfehler nur noch die Summe der Rundungen im Ringspeicher. Ich würde vermuten, dass die sehr klein sind, man täuscht sich aber leicht.

Integer-haben beim aufsummieren wie hier eine höhere Auflösung als Fließpunkt . Es ist nur mehr Aufwand, Integer richtig zu skalieren (also Fixpunkt ohne Überlauf)

Wenn ich den Integer-Weg gehe, soll sich das trotzdem über Gleitkomma-Parameter in das Programm einfügen. Einfach um die Logik des übergeordneten Programms nicht zu verkomplizieren.

OP #7548172
Lesenswert?

Was haltet ihr davon, die letzten paar Stellen der Gleitkommamantisse vor dem Reinstecken abzuschneiden? Soviel abschneiden, dass man beim "Reinstecken" und Rausholen keinen Rundungsfehler mehr haben kann. Bei 4000 Stützstellen müsste man wohl 12 Bit abschneiden (wenn ich mich nicht täusche). Man könnte bspw. eine eingehende Gleitkommazahl zunächst in Single Precision umwandeln, also auf 23 Bit Mantisse und anschließend wieder auf Double (52 Bit Mantisse), die letzten 29 Bit wären also 0. Ich denke, es dürfte dann zu keinem bleibenden Drift mehr kommen. Oder übersehe ich was?

#7548183
Lesenswert?

Ich habe das schon mal so gelöst:

out = summe_mittelwert + in[0] - in[-4000] summe_neu = summe_neu + in[0]

alle 4000 Werte: summe_mittelwert = summe_neu summe_neu = 0

mittelwert = summe_mittelwert / 4000

Damit wird alle 4000 Werte der Rundungsfehler zurückgesetzt. Die Zykluszeit ist immer nahezu gleich.

#7548193
Lesenswert?

Maxe schrieb:

Oder übersehe ich was?

Welchen Sinn macht es, fixpoint in gleitkomma nachzubilden.

Gib einfach Deine Wertebereiche an, dann sind konkrete Aussagen möglich. Höchstwahrscheinlich ist es in Integer nicht aufwändiger. Und in Double zumindestens genauer als in int32. Es summieren sich da auch praktisch keine Fehler auf, wenn es in int gepasst hätte.

#7548195
Lesenswert?

Rolf schrieb:

Wenn der Wert des Divisionsergebnisses xxx,5 ist, dann rundet der von Compiler generierte Code entweder immer auf xxx oder immer auf xxx+1.

Float kennt gar keine xxx,5. Float Zahlen sind Binärzahlen, d.h. entweder ist ein Bit gesetzt oder nicht.

Alles andere ist nur ein Thema der Wandlung zur Anzeige als Dezimalzahl

#7548198
Lesenswert?

Maxe schrieb:

Jetzt frag ich mich, ob man dabei einen ungewollten Drift reinbekommen kann, wenn man mit Gleitkommazahlen arbeitet, weil es beim addieren und subtrahieren des gleichen Wertes evtl. zu Rundungen bzw. ungleichen Rundungsfehlern kommt.

Zum Addieren und Subtrahieren müssen deine Zahlen sowieso auf den gleichen Exponenten normiert werden, d.h. die unteren Bits des reinlaufenden und des rausgeschmissenen Wertes gehen verloren, je mehr, um so länger dein Array ist. Was also versprichst du dir von der Float-Rechnerei?

OP #7548226
Lesenswert?

Michael schrieb:

Ich habe das schon mal so gelöst:

out = summe_mittelwert + in[0] - in[-4000] summe_neu = summe_neu + in[0]

alle 4000 Werte: summe_mittelwert = summe_neu summe_neu = 0

mittelwert = summe_mittelwert / 4000

Damit wird alle 4000 Werte der Rundungsfehler zurückgesetzt. Die Zykluszeit ist immer nahezu gleich.

Perfekt! (Deshalb auch das Vollzitat.)

Das ist auch ein transparenter Algorithmus, d.h. man sieht gleich ob die eigene Implementierung auch wirklich funktioniert.

--

Bruno V. schrieb:

Welchen Sinn macht es, fixpoint in gleitkomma nachzubilden.

Naja, im Großen hätte man immernoch den gesamten Gleitkommabereich. Und der Drift wäre weg. Aber Michaels Vorgehensweise ist definitiv besser.

Gib einfach Deine Wertebereiche an, dann sind konkrete Aussagen möglich.

Wertebereich -500 Watt bis 2000 Watt. Nützt doch nichts, soll ja universell einsetzbar sein.

--

Rainer W. schrieb:

Zum Addieren und Subtrahieren müssen deine Zahlen sowieso auf den gleichen Exponenten normiert werden, d.h. die unteren Bits des reinlaufenden und des rausgeschmissenen Wertes gehen verloren, je mehr, um so länger dein Array ist. Was also versprichst du dir von der Float-Rechnerei?

Float wird halt im übergeordneten Programm verwendet. Nicht erst extra im Gleitenden Mittelwert.

(Firma: Wilhelm.Consulting) #7548230
Lesenswert?

Maxe schrieb:

Allerdings möchte ich bei dem Filter wie es nur geht driftfrei sein.

Sorry, aber was soll da driften?

Wenn du das "richtig" implementierst, dann driftet da nix.

Nochmal, du hast eigentlich

NeuerWert=(AlterWert*N+Neues_Sample)/(N-1)

Das ist eine konvergente Folge! Die kann dir nicht wesentlich über die 7. Stelle "driften" (bedingt durch 32-bit float; da spielen dann die tatsächlichen Werte und die größe von N mit rein...). Da ist auch kein Ringbuffer notwendig.

Mit Ringbuffer ist das dann eine etwas andere Klasse... aber auch da kann nix driften.

Maxe schrieb:

Ein Extrembeispiel wäre, wenn der Durchschnittswert beim "Reinstecken" des Wertes so groß ist, dass die kleine zugefügte Zahl komplett verschwindet. Also dass das kleinste Bit der Mantisse wertmäßig noch größer ist als der ganze zugefügte Wert. Zum Zeitpunkt, wo der Wert wieder aus dem Mittelwert fällt, könnte der Durchschnitt klein genug sein, dass sich der Wert doch wieder abbildet. Dann hätte man zwischen Reinstecken und Rausholen einen negativen Drift.

Das geht in der "richtigen" Implementierung nicht. Im Code oben, wäre es entweder mit großem AlterWert oder großem N möglich, dass in der Addition das neueste Sample "unter geht". Durch das "/(N-1)", gibt's aber wieder keine drift. "irgendwann" (Tiefpass) verringert sich der Wert soweit, dass alles wieder zum Wert von Neues_Sample konvergiert.

Mit beliebiger Genauigkeit könntest du natürlich AlterWert - AlterWert/N + NeuerWert/N rechnen. Aber genau hier kommt es zum beschriebenen Problem mit Runden.

Das ist aber auch kein "richtiger" Moving Average (siehe Wikipedia Link)!

Die mathematischen Hintergründe findet man, wenn man sich IIR und FIR Filter ansieht. Über alle Elemente vom Ringbuffer mitteln wäre ein FIR. Der ist immer stabil. Die Sache ohne Ringbuffer wäre IIR. Da muss man etwas aufpassen.

Wenn du das nicht mit "*N/(N-1)" machst, dann bekommst du einen Pol, der sich negativ bemerkbar machen kann.

73

OP #7548500
Lesenswert?

Hans W. schrieb:

Nochmal, du hast eigentlich NeuerWert=(AlterWert*N+Neues_Sample)/(N-1)

Nein, das ist ein ganz anderer Filter, um den es hier überhaupt nicht geht.

Die mathematischen Hintergründe findet man, wenn man sich IIR und FIR Filter ansieht.

Die Frage des Driftens ist m.E. nicht eine der Filtertheorie, sondern eine Frage der konkreten Implementierung. Einen Integrator könnte man rechnerisch (Assoziativregel) auf zwei parallele aufteilen, und hätte - rechnerisch - den gleichen Ausgang. Tatsächlich würden sie dann aber auseinanderlaufen und damit in der Praxis nicht funktionieren. (Das nur als Beispiel).

Ich würde mich auch überzeugen lassen, dass die Gleitkommazahlen sowieso nicht abdriften können, aber dann braucht es schon stichhaltige Argumente.

Purzel H. schrieb:

Was waere denn der Vorteil eines Gleitenden Averages ueber die letzten 256 Werte, gegen den exponentiellen Average ueber die letzten 256 Werte ?

Was ist denn ein "exponentieller Average über 256 Werte"?

Ein Moving Average ist u.a. dann sinnvoll, wenn man eine einzige bekannte Frequenz herausfiltern möchte. Bspw. die Zacken eines PWM-Signals lassen sich mit einem passenden Moving Average komplett entfernen.

(Firma: Wilhelm.Consulting) #7548566
Lesenswert?

Maxe schrieb:

Hans W. schrieb:

Nochmal, du hast eigentlich NeuerWert=(AlterWert*N+Neues_Sample)/(N-1)

Nein, das ist ein ganz anderer Filter, um den es hier überhaupt nicht geht.

Stimmt. Aber ein richtiger Moving Average ist eben so definiert!

Was ihr mach ist eine "optimierte" Variante.

Typischerweise machst du das so wie ihr wenn du mit 2^N Basis arbeitest. Dann brauchst dir Addition und Schiebeoperationen.

In float bekommst du damit aber ggf Probleme.

Wenn schon Moving Average in floating point, dann implementiere das bitte so, wie es eigentlich definiert ist. Damit ist das stabil und "driftet" nicht und ist alles in allem ein gutartiger Tiefpass.

Die taktmäßig optimierte Version ist eben nicht unter allen Bedingungen optimal.

Der Wikipedia Artikel ist schon recht informativ.

73

#7548813
Lesenswert?

Hans W. schrieb:

Nochmal, du hast eigentlich

NeuerWert=(AlterWert*N+Neues_Sample)/(N-1)

Das ist eine konvergente Folge!

Nein, das ist erstens kein gleitender Mittelwert, sondern ein IIR-Filter und zweites auch noch falsch. Wenn, dann muss es heißen .../(n+1), sonst konvergiert da gar nichts.

Hans W. schrieb:

Aber ein richtiger Moving Average ist eben so definiert!

Nein, ein Moving Average ist ein FIR-Filter. Der gefilterte Wert ergibt sich als Mittelwert der letzten n Samples und alle Sample sind gleich gewichtet. https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average

#7548873
Lesenswert?

Rainer W. schrieb:

Rolf schrieb:

Wenn der Wert des Divisionsergebnisses xxx,5 ist, dann rundet der von Compiler generierte Code entweder immer auf xxx oder immer auf xxx+1.

Float kennt gar keine xxx,5. Float Zahlen sind Binärzahlen, d.h. entweder ist ein Bit gesetzt oder nicht.

Ad1: Ich habe auf ein Posting geantwortet, in dem eben nicht mit Float, sondern mit Integer gerechnet wurde.

Ad2: Ich habe "Wert" gesagt. Selbstverständlich kann eine Float-Variable den Wert xxx,5 haben. Gerade WEIL Exponent und Mantisse Dualzahlen sind. https://www.h-schmidt.net/FloatConverter/IEEE754de.html

(Firma: Wilhelm.Consulting) #7549005
Lesenswert?

Rainer W. schrieb:

Wenn, dann muss es heißen .../(n+1)

Uppps. Asche auf mein Haupt! Natürlich völlig richtig. Der Term mit der Historie muss mit kleiner 1 gewichtet sein.

Rainer W. schrieb:

Nein, ein Moving Average ist ein FIR-Filter.

Jain. Es gibt auch den IIR Ansatz... das ist zwar dann eigentlich eher ein klassischer Tiefpass, aber es ist auch ein Moving Average.

Aus der Einleitung des verlinkten Artikels:

1
Variations include: simple, cumulative, or weighted forms (described below).

Jedenfalls kann sowas weder driften, noch oszillieren noch sonst was.

Das was da oben beschrieben wurde sind auch IIR Filter, weil da die Historie mit verrechnet wird.

Siehe hier:

Maxe schrieb:

Michael schrieb:

Ich habe das schon mal so gelöst: out = summe_mittelwert + in[0] - in[-4000] summe_neu = summe_neu + in[0] alle 4000 Werte: summe_mittelwert = summe_neu summe_neu = 0 mittelwert = summe_mittelwert / 4000 Damit wird alle 4000 Werte der Rundungsfehler zurückgesetzt. Die Zykluszeit ist immer nahezu gleich.

Perfekt! (Deshalb auch das Vollzitat.)

Ich will da jetzt nicht irgendwie klugscheißen... Ich wollte nur darauf hinweisen, dass ein richtig ausgesuchter Moving Average einfach nicht driftet. Wenn er es tut, dann ist es entweder eine ungeeignete Variante, oder man hat sich vertippt (wie ich mit dem dummen -/+ Fehler)!

Wie dem auch sei, wenn es die Rechenleistung und der Speicher erlaubt, würde ich einfach über alle Elemente in einem Ringbuffer drübermitteln. Das macht am wenigsten numerische Fehler/Probleme.

Nebenbei (wenn das Rauschen mitspielt) kann man damit direkt seine Auflösung erhöhen... Einfach 2^N Samples addieren und aufs schieben vergessen. Das ist aber wieder ein ganz anderes Thema mit seinen eigenen Problemen.

73

#7549038
Lesenswert?

Hans W. schrieb:

Wie dem auch sei, wenn es die Rechenleistung und der Speicher erlaubt, würde ich einfach über alle Elemente in einem Ringbuffer drübermitteln.

Für die nummerischen Fehler ist es hier völlig egal, in welcher Reihenfolge man rechnet, solange man die Fehler nicht provoziert. Jedes Mal n-1 Element noch einmal stumpf aufzusummieren ist so ziemlich der dämlichste Ansatz, wenn es doch die Möglichkeit gibt, die Summe rekursiv aus der vorherigen zu berechnen. Die letzten n Samples muss man bei einem FIR-Filter so oder so im Speicher bereit halten.

Das macht am wenigsten numerische Fehler/Probleme.

Wieso macht das weniger Fehler als eine rekursive Berechnung der Summe, die letztendlich mit den selben Operationen bestimmt wird, nur in unterschiedlicher Reihenfolge?

(Firma: Wilhelm.Consulting) #7549117
Lesenswert?

Rainer W. schrieb:

Wieso macht das weniger Fehler als eine rekursive Berechnung der Summe

Weil bei floats die Reihenfolge der Rechenoperationen eben nicht (ganz) egal ist.

Ich bezweifle aber, dass man oft den Fall hat, dass die Input Samples sich um 4-5 Größenordnungen unterscheiden... Erst dann wirst du beim float, mit seinen 7 signifikanten Stellen, relevante Unterschiede sehen.

73

Gast #7549123
Lesenswert?

Rainer W. schrieb:

Jedes Mal n-1 Element noch einmal stumpf aufzusummieren ist so ziemlich der dämlichste Ansatz, wenn es doch die Möglichkeit gibt, die Summe rekursiv aus der vorherigen zu berechnen.

Sicher?

Ein paar Integer zu addieren kann durchaus schneller gehen, als die alternative (mit Division und so). Vor allem ist es ein geradliniger Ansatz, den jeder Programmierer versteht, ohne einen Knoten im Kopf zu bekommen. Man soll schließlich wartbaren Code schreiben, damit nach uns nicht die Sintflut kommt.

Gast #7549305
Lesenswert?

Hallo Steve,

Steve van de Grens schrieb:

Sicher?

Ein paar Integer zu addieren kann durchaus schneller gehen, als die alternative (mit Division und so).

Er meinte vermutlich meine bereits in Beitrag "Re: Gleitender Mittelwert -> Algorithmus" dargestellte Variante.

Dividieren musst du an irgend einer Stelle auf jeden Fall, sonst wird es schwierig, einen Mittelwert zu berechnen. Und 4000 Integer zu addieren dauert auch so seine Zeit, bzw. ist eine Division schnell erledigt, je nach CPU-Architektur.

ciao

Marci

#7549804
Lesenswert?

Marci W. schrieb:

Und 4000 Integer zu addieren dauert auch so seine Zeit

Früher®, als die Prozessrechner noch viel größer aber viel langsamer waren (Minis à la DEC PDP), musste man viel mehr tricksen als heute.

Im konkreten Fall hier hätte man die Addition vielleicht schon begonnen, bevor der letzte Summand bekannt war. Dann bekäme man den Mittelwert fast sofort nach der letzten Messung.

Gast #7549824
Lesenswert?

Rolf schrieb:

Marci W. schrieb:

Und 4000 Integer zu addieren dauert auch so seine Zeit

Früher®, als die Prozessrechner noch viel größer aber viel langsamer waren (Minis à la DEC PDP), musste man viel mehr tricksen als heute.

Naja, ist halt die Frage wie schnell resp. "echtzeitig" die SPS sein muss. Bei der angesprochenen ca. 50 Jahren alten DEC PDP kann man von 0.3 MIPS ausgehen, da sollte immer noch time slots in Zehntelsekunden ermöglichen. Ggf. speichert man Teilsummen als Zwischenwerte.

#7550686
Lesenswert?

Maxe schrieb:

Hallo zusammen, ich brauch für die SPS einen Moving-Average-Filter/Gleitenden Mittelwert. Das ist ja eigentlich auch leicht und effizient zu programmieren, indem man dem Durchschnitt jeweils den neu hinzukommenden Wert anteilig addiert und den herausfallenden Wert anteilig subtrahiert.

Kann mir jemand erklären, wozu das gut sein soll, für den gleitenden Mittelwert eine Menge Daten zu sammeln, die dann alle gleich gewichtet werden?

Ich benutze zum Glätten von Kurven einen einfachen Tiefpass, der außer dem Mittelwert selbst nur noch den neuen Wert braucht. Die Differenz des neuen Wertes gegenüber dem Mittelwert wird durch eine Konstante dividiert und zum Mittelwert addiert. Fertig ist der Algorithmus.

Dadurch ergibt sich automatisch eine exponentiell abnehmende Gewichtung für die älteren Werte, was wiederum zu einer besseren Glättung und besserer Aktualität führt.

#7550713
Lesenswert?

Maxe schrieb:

Eine Regelung in der Verfahrenstechnik, das Fluid ist 3 Minuten in der Leitung unterwegs, also 180 Sek. Bei 10 Zyklen pro Sekunde hat man 1800 Werte.

Du schreibst von einer Regelung und einer Totzeit von 180 Sekunden. Warum nimmst du dann eine so kurze Abtastzeit? Ich habe vor langer Zeit in der Vorlesung Digitale Regelungstechnik gehört dass man die Abtastzeit auf die Geschwindigkeit der Regelstrecke und des Reglers anpassen soll. Mit einer so kurzen Abtastzeit bekommst du doch auch extrem unhandlich kleine Parameter. Wenn ich mich recht erinnere war eine gute Faustregel die Abtastzeit 10 mal kleiner wie die relevanten Zeitkonstanten zu wählen. Und nicht fast 2000-fach wie bei dir.

Persönliche Seite #7555454
Lesenswert?

Michael D. schrieb:

Wahrscheinlich ist es dasselbe was J. S. vorgeschlagen hat.

eher nicht, das hier z.B. bringt wieder das Problem zurück:

sum = sum - ((sum + count/2) / count) + next;

und auch das hier :

is the '+count/2' for 5/10 rounding correct here?

löst das Problem nicht. Wenn man den Fehler integriert, bekommt man irgend ein niederfrequentes Spektrum und das widerspricht dem gesamten Ansatz, per Mittelung eine Filterung vorzunehmen und nur die Höhen abzuschneiden: ("high cut"): Die Wahl der Anzahl der Werte, mit denen man mittelt, bemisst sich ja anhand einer Frequenzbetrachtung d.h. die Grenzfrequenz ist mit der Zahl verknüpft. Das darf man nicht konterkarieren. Der Rundungsfehler darf nur lokal auftreten, nach der jeweiligen Division.

(Firma: Wilhelm.Consulting) #7557652
Lesenswert?

Nochmal: Das Problem sind die Floats!

Dort sind durch das prinzipbedingte Skalieren die Additionen nicht Assoziativ. Damit macht es einen (kleinen) Unterschied, ob du das 1. Sample zuerst mit dem Rest addierst und dann abziehst, oder es garnicht mitzählst.

Hast du die Möglichkeit die Integer-Samples vom ADC zu verwenden und erst nach dem Mittelwert zum Float zu machen, dann hast du diesen Fehler nicht.

Das ist in der Analog-Devices Appnote oben beschrieben.

Ansonsten würde ich z.B. auf dspguide.com verweisen... so ein gleitender Mittelwert (FIR Filter mit konstanter Gewichtung) hat eigentlich vergleichsweise schlechte Eigenschaften... Filterwirkung naja, Berechnung naja, Speicherbedarf naja

73

Persönliche Seite #7559867
Lesenswert?

Hans W. schrieb:

Berechnung naja, Speicherbedarf naja

Bei "Filterwirkung NAJA" gehe ich voll mit, aber ich wüsste nicht, welcher gleitende Filter weniger Rechen- und Speicherbedarf hat. Musst ihn natürlich nicht als FIR mit ausdrücklichen Werten - sondern als CIC2 laufen lassen. 2 Akkumulatoren sind das Minimale. Kleiner geht nur ein IIR mit Ganzzahlkoeffizienten.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren