stumpfer Brute-Force Morse-Code Dekoder

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

folgende Aufgabenstellung: es gibt Morse-Code, wo aller Leerzeichen fehlen. Was das dekodieren sehr schwierig macht, weil Buchstaben im Morsealphabet zwischen 1 und 4 (nur Buchstaben) bzw. zwischen 1-6 (inkl. Zahlen und Sonderzeichen) lang sein können.

Mein Ansatz für eine relativ stumpfe Brutforce-Dekodierung ist wie folgt:
  • eine Liste mit allen möglichen (und nicht unbedingt sinnvollen) Längenkombinaitonen berechnen
  • prüfen, welche Längenkombination die gleiche Länge wie der Morse-Code haben
  • der Ergebnisliste hinzufügen
Beispiel:
.-.- hatte die gültigen Längenkombinaton (1,1,1,1), (1,2,1), (2,1,1), (1,1,2), (3,1), (1,3), (4) - aber eben nicht z.B. (2,3)

Auf Basis der Längenkombis kann man dann den Morsestring in Teilstring zerlegen und prüfen, wo was sinnvolles & gültiges raus kommt.

Das folgende Skripte findet die gültige Längenkombis:

Code: Alles auswählen

from itertools import product

class MorseCodeEvaluator(object):
    def __init__(self, morse_code, a_to_z_only=False):
        self.morse_code = morse_code
        self.a_to_z_only = a_to_z_only

    def gen_possible_products(self):
        if self.a_to_z_only == True:
            upper_number = 5
        else:
            upper_number = 7
        sequences = product(range(1,upper_number), repeat=len(self.morse_code))
        possibilities = []
        for sequence in sequences:
            sum_ = 0
            temp_seq = []
            for number in sequence:
                sum_ = sum_ + int(number)
                temp_seq.append(number)
                if sum_ == len(self.morse_code):
                    if temp_seq not in possibilities:
                        possibilities.append(temp_seq)
                    break
        return possibilities

if __name__ == '__main__':
    import time
    morse_code = '--.....--.-.-'
    my_eval = MorseCodeEvaluator(morse_code, a_to_z_only=True)
    start = time.asctime(time.localtime(time.time()))
    print('Starting evaluation of morse code.')
    print('Morse code contains {} symbols'.format(len(my_eval.morse_code)))
    my_eval.gen_possible_products()
    end = time.asctime(time.localtime(time.time()))
    print('''length morse code: {0},\
start: {1},\
end: {2}'''.format(len(morse_code), start, end))
... und funktioniert auch. ABER die Laufzeit ist auf meinen nicht mehr ganz taufrischen Thinkpad R61 schon bei 13 Morsesymbolen ca. 30 min. So ist das halt bei stumpfen Brute-Force Berechnungen...

Aber hat einer eine Idee, wie man den Code effizienter = schneller machen kann?

Zum Testen solltet ihr überigens die Länge des Morsecodes auf 10 Zeichen oder so verringern, dann dauert die Berechnung auch nur ein paar Sekunden.

Gruß, noisefloor
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Da der Morse-Code offensichtlich kein Prefix-Code ist, tippe ich einfach mal auf Set-Cover und damit NP-Vollständigkeit.

Code: Alles auswählen

== True
Wirklich? :wink:
Das Leben ist wie ein Tennisball.
BlackJack

@noisefloor: Die Klasse wäre ein prima Kandidat als einfache Funktion umgeschrieben zu werden. Für `possibilities` würde ich ja ein `set()` nehmen. Das wäre das ``in`` nicht nur effizienter, man bräuchte es sogar gar nicht sondern könnte einfach hinzufügen.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Wenn du die Sprache kennst, könnte man sicherlich einige mögliche Dekodierungen mithilfe eines Wörterbuches ausschliessen.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Mal ein weniger kryptischer Vorschlag:

Code: Alles auswählen

ALPHABET = {
    ".-": "A",
    "-...": "B",
    "-.-.": "C",
    "-..": "D",
    ".": "E",
    "..-.": "F",
    "--.": "G",
    "....": "H",
    "..": "I",
    ".---": "J",
    "-.-": "K",
    ".-..": "L",
    "--": "M",
    "-.": "N",
    "---": "O",
    ".--.": "P",
    "--.-": "Q",
    "-.-": "R",
    "...": "S",
    "-": "T",
    "..-": "U",
    "...-": "V",
    ".--": "W",
    "-..-": "X",
    "-.--": "Y",
    "--..": "Z"
}


def consume(code):
    if not code:
        yield ()
    else:
        for morse, letter  in ALPHABET.iteritems():
            if code.startswith(morse):
                for sub in consume(code[len(morse):]):
                    yield (letter,) + sub


def main():
    code = "--.....--.-.-"

    for message in set(consume(code)):
        print "".join(message)


if __name__ == "__main__":
    main()
Aus dem Codewörtern könnte man natürlich vorher noch einen Baum basteln, bei nur 26 (oder 36) Symbolen lohnt sich das aber wohl kaum. Möglicherweise lohnt sich ein Test, ob ein Restcode bereits analysiert wurde.
Das Leben ist wie ein Tennisball.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

EyDu hat geschrieben:Möglicherweise lohnt sich ein Test, ob ein Restcode bereits analysiert wurde.

Code: Alles auswählen

def consume(code, suffixes=None):
    if not code:
        yield ()
    else:
        if suffixes is None:
            suffixes = {}

        for morse, letter  in ALPHABET.iteritems():
            if code.startswith(morse):
                suffix = code[len(morse):]
                try:
                    remainder = suffixes[suffix]
                except KeyError:
                    remainder = set(consume(suffix, suffixes))
                    suffixes[suffix] = remainder

                for sub in remainder:
                    yield (letter,) + sub
Das Leben ist wie ein Tennisball.
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

Danke für die Antworten schon mal.

@ BlackJack: ob das wirklich eine Klasse bleibt weiß ich auch noch nicht. Hatte aber erst mal so angefangen. Das mit dem Set werde ich einbauen.

@DasIch: Das das mal "universell" sein soll, ist die Sprache nicht bekannt.

@EyDu: Danke, muss ich mir mal in Ruhe anschauen, deinen Code.

Eine Idee, die ich noch hatte: Das Problem ist ja ohne Problem parallelisiertbar. Man könnte z.B. vier Threads starten, jeder prüft die möglichen Lösungen, die mit (1,...), (2,....), (3,...) und (4,...) starten (im Fall von `a_to_z_only` = True`). Wie viel das dann bringt habe ich noch nicht probiert.

Gruß, noisefloor
BlackJack

@noisefloor: Threads, zumindest in CPython, werden nichts bringen. Da sollte man sich dann eher `multiprocessing` hernehmen, oder `concurrent.futures` mit einem `ProcessPoolExecutor`.
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

noisefloor hat geschrieben:Eine Idee, die ich noch hatte: Das Problem ist ja ohne Problem parallelisiertbar. Man könnte z.B. vier Threads starten, jeder prüft die möglichen Lösungen, die mit (1,...), (2,....), (3,...) und (4,...) starten (im Fall von `a_to_z_only` = True`). Wie viel das dann bringt habe ich noch nicht probiert.
Die ``threading``-Bibliothek in CPython ist nur für Einkernsysteme ausgelegt. Es wird also niemals ein weiterer Kern hinzugeschaltet. Damit erzeugt es in deinem Fall lediglich Overhead für die Verwaltung der Threads, bringt dir aber keinerlei Vorteile, was die Gesamtlaufzeit für die Berechnungen angeht.

Threading macht deshalb nur dann Sinn, wenn du z.B. mit einer Klasse das Abspielen von Musikstücken realisierst und nicht möchtest, dass der Verarbeitungsfluss bis zum Ende des Tracks blockiert, sobald die ``play``-Methode aufgerufen wurde. Oder es macht Sinn, wenn du große Mengen an Daten von Festplatte, Stick, Netzwerk oder ähnliches Lesen (Herunterladen) bzw Schreiben (Hochladen) musst und dein Programm, während die Schreib- bzw Leseoperation läuft, noch etwas anderes tun soll. Oder für das gleichzeitige Herunterladen von mehreren Dateien aus dem Internet, usw.

Was du hier eher brauchst, ist etwas auf Basis des ``multiprocessing``-Moduls. Dazu hatte BlackJack ja bereits weitere Infos gegeben. Bedenke nur, dass das Starten eines weiteren Prozesses - genau genommen: eines weiteren Python-Interpreters - auch gewissen Overhead erzeugt. Auch das Übermitteln und Zusammenführen der Ergebnisse und das Herunterfahren der zusätzlich gestarteten Interpreter nimmt ein bißchen Zeit ein. Man sollte also wirklich überlegen, ob sich die Parallelisierung überhaupt lohnen würde. Ausgeklügelter Code könnte daher anhand der vorliegenden Datenmenge entscheiden, ob er vorher parallelisiert, oder ob er die Berechnung ohne Parallelisierung durchführt.

Viel Spaß beim Tüfteln. ;)
BlackJack

@snafu: Also das mit `threading` erscheint mir doch ein wenig einseitig beschrieben. Die `threading`-Bibliothek ist auf mehrere *tatsächlich* parallel laufende Threads ausgelegt und ganz bestimmt nicht auf Einkern/-prozessorsysteme. Es wird ein *echter* Systemthread für jedes `Thread`-Objekt erzeugt und die laufen auch ganz normal parallel.

Die Einschränkung kommt bei CPython dadurch das immer nur einer dieser Threads zu jedem gegeben Zeitpunkt Python-Bytecode ausführen kann, und die anderen die das auch gerade tun wollen solange warten müssen. Ein Thread der keinen Python-Bytecode ausführt kann durchaus echt parallel zu den anderen Threads ausgeführt werden wenn er das „global interpreter lock” (GIL) freigibt. Also wenn man C-Erweiterungen hat, die einen deutlichen Anteil mit dem Ausführen von „nativem” Code, der keine Pythoninterpreter-Interna benutzt, kann man durchaus auch einen Geschwindigkeitsvorteil mit Threads erreichen. Numpy wäre ein prominentes Beispiel für eine Erweiterung wo die Entwickler sich Gedanken gemacht haben und machen wo sie das GIL überall freigeben können.
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich sehe jetzt, dass meine Aussage murks war. Die Verteilung auf mehrere Kerne ist möglich. Die Ausführung wird aber zumindest auf Bytecode-Ebene nicht parallel erfolgen, sodass es in dem Fall nicht den erwarteten Effekt hätte. Danke für die Richtigstellung, BlackJack.
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

das mit Threads & Multiprocessing hat sich eh' erledigt, weil der Code von EyDu sehr gut = sehr sehr sehr schnell ist.
Selbst für 15 Symbole ist die Lösung nach <1s da :-) Gut, man muss dann noch ein paar Tausend mögiche Worte durchgehen, aber dass müsste man bei meiner Methode auch ;-)

@EyDu: Vielen Dank für den Code!

Und ich sollte mich doch mal mehr mit Generatoren beschäftigen.

Übringes & noch zum Abschluss: bei Stack Overflow gibt's auch ein paar Threads dazu, aber die Lösung von EyDu ist die bisher eleganteste, IMHO.

Gruß, noisefloor
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

noisefloor hat geschrieben:Gut, man muss dann noch ein paar Tausend mögiche Worte durchgehen, aber dass müsste man bei meiner Methode auch ;-)
Wenn du die Sprache der Nachrichten kennst (und vielleicht noch die üblichen Abkürzungen beim Morsen), dann könntest du ein Wörterbuch gegen die möglichen Nachrichten laufen lassen. Am einfachsten wäre es wohl wenn du für jedes Wort aus dem Wörterbuch testest, ob es in der Nachricht vorkommt. Am Ende lässt du dir die k Nachrichten ausgeben, welche am meisten Wörter enthalten. Ist natürlich nur eine sehr grobe Heuristik, ist aber auch in einer halben Stunde implementiert.

Ansonsten kannst du natürlich auch gegen das Wörterbuch matchen, was aber sehr rechenintensiv ist. Fehler und unbekannte Wörter können wohl nicht ausgeschlossen werden. Wenn die erstgenannte Heuristik gut funktionieren sollte, könntest du das Matching dann auf die k besten Ergebnisse reduzieren.

Ob sich der ganze Aufwand lohnt hängt natürlich davon ab, wie viele Nachrichten du hast. Und ob du neue Techniken lernen möchtest.
Das Leben ist wie ein Tennisball.
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

das ganze hat übrigens keinen so richtig konkreten Hintergrund. Die Motivation war, ein Rätsel (=Morsecode ohne Leerzeichen) mit Python zu lösen. Und in dem Rätsel kommen rel. kurze Worte vor - damit man auch noch von Hand in akzeptabler Zeit dekodieren kann.

Praktisch funktioniert das Ganze ja auch wirklich nur mit ganzen Wörtern. Wenn man Morsecode hätte, wo die Leerzeichen den Zeichen und den Worten fehlen, dann funktioniert das Matchen gegen ein Wörterbuch ja auch schon nicht mehr.

Man könnte die Liste der mögliche Worte vllt. noch dadurch reduzieren, dass man Wörtern mit Buchstaben-Kombis, die es nicht gibt (im Deutschen z.B. drei gleiche Vokale hintereinander - im finnischen gibt's das glaube ich ;-) ) raus filtert.

Gruß, noisefloor
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

Nachtrag zum Code von EyDu: irgendwas stimmt da nicht, der Code findet nicht alle gültigen Kombis.

Beispiel: das Wort POSTEN
Das ist .--. --- ... - . -., also .--.---...-.-. ohne Leerzeichen. Läßt man das durch das Skript laufen, dann findet es _nicht_ AKZENTE als gültige Folge - ist es aber. .- -.- --.. . -. - . mit Leerzeichen, .--.---...-.-.

Woran auch immer das liegen mag...

Gruß, noisefloor
Benutzeravatar
sparrow
User
Beiträge: 4195
Registriert: Freitag 17. April 2009, 10:28

Das Alphabet stimmt nicht:

Code: Alles auswählen

>>> len(ALPHABET)
25
Und Buchstaben die er nicht kennt, kann er auch nicht erkennen.
R und K sehen ausgesprochen ähnlich aus, was das Alphabet im Beispiel angeht.
Und deshalb findet er auch nicht "AKZENTE" aber durchaus "ARZENTE" ;)
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

ah ja... ich hatte nichte alle Morse-Sequenzen geprüft. Danke für den Hinweise!

Die Länge des Alphabets stimmt aber:

Code: Alles auswählen

>>> import morse_eval as me
>>> me.ALPHABET
{'-.-': 'K', '-.': 'N', '-...': 'B', '....': 'H', '-..-': 'X', '---': 'O', '-': 'T', '--..': 'Z', '..': 'I', '..-': 'U', '.--.': 'P', '...-': 'V', '.-.': 'R', '.--': 'W', '.-..': 'L', '--': 'M', '.': 'E', '-.--': 'Y', '--.': 'G', '.-': 'A', '.---': 'J', '--.-': 'Q', '-.-.': 'C', '...': 'S', '-..': 'D', '..-.': 'F'}
>>> len(me.ALPHABET)
26
Zumindest in meiner Version ;-)

Hier nochmal das Listing für Python 3, was jetzt richtig arbeiten sollte:

Code: Alles auswählen

ALPHABET = {
".-": "A",
"-...": "B",
"-.-.": "C",
"-..": "D",
".": "E",
"..-.": "F",
"--.": "G",
"....": "H",
"..": "I",
".---": "J",
"-.-": "K",
".-..": "L",
"--": "M",
"-.": "N",
"---": "O",
".--.": "P",
"--.-": "Q",
".-.": "R",
"...": "S",
"-": "T",
"..-": "U",
"...-": "V",
".--": "W",
"-..-": "X",
"-.--": "Y",
"--..": "Z"
}
     
     
def consume(code, suffixes=None):
    if not code:
        yield ()
    else:
        if suffixes is None:
            suffixes = {}
  
        for morse, letter  in ALPHABET.items():
            if code.startswith(morse):
                suffix = code[len(morse):]
                try:
                    remainder = suffixes[suffix]
                except KeyError:
                    remainder = set(consume(suffix, suffixes))
                    suffixes[suffix] = remainder
     
                for sub in remainder:
                    yield (letter,) + sub
     
def main():
    code = ".--.---...-.-." #POSTEN
     
    for message in set(consume(code)):
        print("".join(message))
     
     
if __name__ == "__main__":
    main()
Gruß, noisefloor
Antworten