Ohjelmointihaaste: Generatiivinen palidromikone

Jari Hiltunen
15 min readSep 16, 2024

--

Generatiivinen tekoäly, kuten ChatGPT, ei osaa tuottaa palindromeja kovinkaan hyvin, vaikka mallit ymmärtävät eri sanojen läheisyyteen liittyvän todennäköisyyden. Oppiakseni uutta, halusin testata, voisiko koneoppimisen (ML) avulla kehittää mallin, joka tuottaisi palindromeja automaattisesti.

Palindromin tekeminen ihmisen näkökulmasta on luontevaa. Aloitamme usein jollakin sanalla, esimerkiksi “ISO”, ja pohdimme, miten siihen voisi lisätä kirjaimia niin, että takaperin luettuna se muodostaisi järkevän sanan. Tässä kohtaa voimme lisätä kirjaimen T ja saamme sanan “ISOT”, joka takaperin on “TOSI”. Tämä ei kuitenkaan vielä ole palindromi, koska sanat eivät ole täysin identtisiä. Ihminen etsisi T-kirjaimella alkavaa sanaa ja voisi päätyä vaikka sanaan TILA. Näin saataisiin ISO TILA, joka on ALI TOSI toisinpäin, eli palindromi. Prosessi vaatii sanan jatkamista ja muokkaamista, kunnes löydämme symmetrisen kokonaisuuden. Tämä inhimillinen prosessi on intuitiivinen ja dynaaminen, mutta haastava koneelle.

Koneoppimisen rajoitukset palindromien luonnissa

Koneoppiminen, kuten LSTM-mallit (pitkäkestoiset muistit), oppii tunnistamaan todennäköisyyksiä ja sekvenssien riippuvuuksia. Kuitenkin palindromien kohdalla kyse on tiukasta symmetriasta — jokainen kirjain tai sana peilaa itseään käänteisessä järjestyksessä. LSTM ei luonnostaan käsittele tällaista rakenteellista vaatimusta. Vaikka koneoppiminen voi auttaa löytämään todennäköisiä sanayhdistelmiä, se ei luonnollisesti tunnista symmetrisyyden vaatimaa tiukkaa rakennetta.

Ensimmäiset yritykset: satunnaisten sanojen yhdistely

Lähestyin ongelmaa yksinkertaisella ohjelmointitestillä, jossa yritin satunnaisesti yhdistellä sanoja (adjektiiveja, verbejä, substantiiveja) ja tarkastella, syntyykö niistä palindromeja. Tämä osoittautui kuitenkin tehottomaksi, sillä palindromin löytyminen satunnaisotannalla on harvinaista. Käytännön testien aikana sain tuloksia, joissa oli satunnaisia sanoja:

Lause 'laiskansitkeä kevytsora heilahdella' ei ole palindromi.
Lause 'eklektinen sinologia tuurata' ei ole palindromi.
Lause 'urheilullinen panna murahtaa' ei ole palindromi.

Tämä lähestymistapa ei ollut tehokas, koska satunnainen yhdistely ei johda symmetriseen rakenteeseen.

Oppimismalli palindromien tunnistamiseen

Lähdin liikkeelle myös oppimismallin rakentamisesta, jossa syötin neuroverkolle sanoja ja lauseita, jotka joko olivat tai eivät olleet palindromeja. Mallin tavoitteena oli oppia erottamaan palindromiset lauseet ei-palindromisista. Tämä sisälsi sanojen puhdistamisen, välimerkkien poistamisen ja lopulta sekvenssien analysoinnin.

# Esimerkki koodista, joka tarkistaa palindromit
def is_palindrome(text):
return text == text[::-1]

Näiden kokeilujen perusteella kävi kuitenkin ilmi, että koneoppimisella on vaikeuksia tunnistaa symmetriaa. Koneen tehtävänä on oppia todennäköisyyksiä, mutta palindromien vaatima symmetria on monimutkaisempaa ja vaatii perinteisiä loogisia tarkistusalgoritmeja.

Hybridimalli: Koneoppimisen ja logiikan yhdistäminen

Yksi mielenkiintoinen ratkaisu voisi olla hybridimalli, jossa käytetään sekä koneoppimista että loogista tarkistusta. Kone voisi esimerkiksi ehdottaa sanayhdistelmiä perustuen sanojen frekvenssiin ja käyttöyhteyksiin, ja sitten algoritmi tarkistaisi, onko syntyvä lause symmetrinen. Tämä lähestymistapa voi hyödyntää koneoppimista nopeuttamaan ehdotuksia, mutta lopullinen tarkistus tapahtuisi sääntöpohjaisesti.

Kokeiluja ja havaintoja:

  1. Koneoppimisen rajoitukset: LSTM-mallin testaaminen palindromien generoinnissa ei tuottanut toivottuja tuloksia, koska malli ei ole herkkä käänteiselle rakenteelle. Malli saattoi tunnistaa osittaisia palindromeja tai antaa väärin positiivisia tuloksia.
  2. Sanapohjainen lähestymistapa: Satunnaisesti valittujen sanojen kokeilu ei ollut tehokas tapa luoda palindromeja. Palindromin generointi vaatii erityisen rakenteen, joka ei ole satunnaisen yhdistelyn saavutettavissa.
  3. Hybridimalli voi olla lupaava: Yhdistämällä koneoppimista ja loogista tarkistusta, voimme kehittää menetelmän, jossa kone ehdottaa todennäköisiä sanayhdistelmiä ja sitten looginen algoritmi tarkistaa, onko lause palindromi.

Näiden testien kautta on selkeää, että vaikka tekoäly ja koneoppiminen tarjoavat monia mahdollisuuksia, palindromien generointi vaatii vielä erikoistuneempia menetelmiä. Tekoäly voi ehdottaa sanayhdistelmiä, mutta palindromien erityisluonne vaatii sääntöpohjaisen rakenteen varmistamista. Koneoppiminen voi auttaa nopeuttamaan prosessia, mutta pelkästään todennäköisyyksien perusteella palindromin luominen on haastavaa.

Seuraavana hieman erilaisia testattuja malleja. Aluksi halusin tehdä ohjelman, joka lataa nykysuomen sanalistan eroteltuna verbeihin, adjektiiveihin ja substantiiveihin. Samalla myöhempää opettamista varten latasin kaksi kirjaa tekstitiedostoksi ja etsin listan palindromeista.

import csv
import re

# Ladataan sanat CSV-tiedostoista
def load_words(file_name):
with open(file_name, newline='') as f:
reader = csv.reader(f)
words = [row[0] for row in reader if row] # Valitaan vain sana, ei tyhjiä rivejä
return words

# Ladataan rivit CSV-tiedostoista
def load_text_rows(file_name):
with open(file_name, "r") as file: # comma separated values
text_rows = list(csv.reader(file, delimiter=","))
# Puristetaan listasta listojen sisältö, jotta ei ole sisäkkäisiä listoja
text_rows = [item for sublist in text_rows for item in sublist if item]
return text_rows

# Ladataan lauseet tavallisesta tekstitiedostosta
def load_sentences(file_name):
with open(file_name, 'r', encoding='utf-8') as f:
sentences = f.readlines() # Lue rivit listaksi
sentences = [line.strip() for line in sentences if line.strip()] # Poistetaan tyhjät rivit
return sentences

def clean_text(text):
# Poistetaan kaikki paitsi aakkoset
text = re.sub(r'[^a-zA-ZäöåÄÖÅ]', '', text)
return text.lower() # Muutetaan myös kaikki kirjaimet pieniksi

# Duplikaattien poisto säilyttäen alkuperäinen järjestys
def remove_duplicates(input_list):
seen = set() # Set muistaa jo havaitut elementit
unique_list = []
for item in input_list:
if item not in seen:
unique_list.append(item)
seen.add(item)
return unique_list

def is_palindrome(text):
return text == text[::-1]

# Haetaan tiedot
verbs = load_words('verbi_sanat.csv') # Lue csv sanat
adjectives = load_words('adjektiivi_sanat.csv') # Lue csv sanat
substantives = load_words('substantiivi_sanat.csv') # Lue csv sanat
palindromes = load_text_rows('palindromeja.csv') # Lue csv rivit
long_sentences = load_sentences('LongText.txt') # Lue lauseet

# Puhdistetaan tiedot
for_verbs = [clean_text(x) for x in verbs]
for_adjectives = [clean_text(x) for x in adjectives]
for_substantives = [clean_text(x) for x in substantives]
for_palindromes = [clean_text(x) for x in palindromes]
for_long_sentences = [clean_text(x) for x in long_sentences]

# Varmistetaan, että on uniikkeja listoja
clean_verbs = remove_duplicates(for_verbs)
clean_adjectives = remove_duplicates(for_adjectives)
clean_substantives = remove_duplicates(for_substantives)
clean_palindromes = remove_duplicates(for_palindromes)
clean_long_sentences = remove_duplicates(for_long_sentences)

# Sanat ja palindromit eriteltyinä
verb_palindromes = []
verb_non_palindromes = []
adjective_palindromes = []
adjective_non_palindromes = []
substantive_palindromes = []
substantive_non_palindromes = []
palindromes_list = []
non_palindromes = []
long_sentence_palindromes = []
long_sentence_non_palindromes = []

# Tarkistetaan onko palindromi vai ei, jaotellaan sen mukaisesti
for x in clean_verbs:
if is_palindrome(x):
verb_palindromes.append(x)
else:
verb_non_palindromes.append(x)

for x in clean_adjectives:
if is_palindrome(x):
adjective_palindromes.append(x)
else:
adjective_non_palindromes.append(x)

for x in clean_substantives:
if is_palindrome(x):
substantive_palindromes.append(x)
else:
substantive_non_palindromes.append(x)

for x in clean_palindromes:
if is_palindrome(x):
palindromes_list.append(x)
else:
non_palindromes.append(x)

for x in clean_long_sentences:
if is_palindrome(x):
long_sentence_palindromes.append(x)
else:
long_sentence_non_palindromes.append(x)

# Tulostetaan tulokset
print("Verb palindromes:", verb_palindromes)
input("Seuraava listaus, paina enter")
print("Verb non-palindromes: ", verb_non_palindromes)
input("Seuraava listaus, paina enter")
print("Adjective palindromes: ", adjective_palindromes)
input("Seuraava listaus, paina enter")
print("Adjective non-palindromes: ", adjective_non_palindromes)
input("Seuraava listaus, paina enter")
print("Substantive palindromes: ", substantive_palindromes)
input("Seuraava listaus, paina enter")
print("Substantive non-palindromes: ", substantive_non_palindromes)
input("Seuraava listaus, paina enter")
print("Palindromes list: ", palindromes_list)
input("Seuraava listaus, paina enter")
print("Non-palindromes: ", non_palindromes)
input("Seuraava listaus, paina enter")
print("Long sentence palindromes: ", long_sentence_palindromes)
input("Seuraava listaus, paina enter")
print("Long sentence non-palindromes: ", long_sentence_non_palindromes)

Ohjelma löysi suomenkielen sanoista seuraavanlaisia palindromeja. Suurin osa on tietysti ei-palindromeja:

Verb palindromes: ['autioitua']
Adjective palindromes: ['sees']
Substantive palindromes: ['ajaja', 'akka', 'ala', 'bb',
'dvd', 'ege', 'ele', 'enne', 'imaami', 'isi', 'olo', 'opo',
'otto', 's', 'sammas', 'suhus', 'suuruus', 'sylys', 'sytytys',
'syys', 'syöppöys', 'tet', 'utu', 'www', 'yty', 'ällä', 'ämmä', 'ässä']

Palindromisanoilla olisi helppo rakentaa lausetta keskeltä aloittaen ja voisi olettaa, että ne sopisivat myös palindromin päihin.

Sitten oli aika testata lähestymistapaa, jossa etsitään neuroverkon (Tensorflow ja Keras) avulla sanojen lähekkäisyyksiä käyttämällä pitkää kirjoista otettua tekstiä.

import tensorflow as tf
from tensorflow.keras import layers
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import csv
import os
import pickle

# Ladataan sanaluokat CSV-tiedostoista
def load_words(file_name):
with open(file_name, newline='') as f:
reader = csv.reader(f)
words = [row[0] for row in reader if row] # Valitaan vain sana, ei tyhjiä rivejä
return words

# Ladataan lauseet tavallisesta tekstitiedostosta
def load_sentences(file_name):
with open(file_name, 'r', encoding='utf-8') as f:
sentences = f.readlines() # Lue rivit listaksi
sentences = [line.strip() for line in sentences if line.strip()] # Poistetaan tyhjät rivit
return sentences

# Ladataan dataa
verbs = load_words('verbi_sanat.csv')
adjectives = load_words('adjektiivi_sanat.csv')
substantives = load_words('substantiivi_sanat.csv')
long_sentences = load_sentences('LongText.txt') # Lue lauseet

# Yhdistetään sanat yhdeksi listaksi (sanaluokkamalli)
all_words = verbs + adjectives + substantives

# Tokenizer muuntaa sanat sekvensseiksi (sanaluokkamalli)
tokenizer_words = tf.keras.preprocessing.text.Tokenizer()
tokenizer_words.fit_on_texts(all_words)

# Tokenizer pitkille lauseille (lausemalli)
tokenizer_sentences = tf.keras.preprocessing.text.Tokenizer()
tokenizer_sentences.fit_on_texts(long_sentences)

# Muunna sekvensseiksi ja kouluta sanaluokkamalli
input_sequences_words = tokenizer_words.texts_to_sequences(all_words)

# Padataan sekvenssit yhtenäiseksi pituudeksi
input_sequences_words = tf.keras.preprocessing.sequence.pad_sequences(input_sequences_words)

# Muunna sekvensseiksi ja kouluta lausemalli
input_sequences_sentences = tokenizer_sentences.texts_to_sequences(long_sentences)

# Padataan sekvenssit yhtenäiseksi pituudeksi
input_sequences_sentences = tf.keras.preprocessing.sequence.pad_sequences(input_sequences_sentences)

# Tarkista, onko mallit tallennettu levylle
words_model_file = 'words_model.keras'
sentences_model_file = 'sentences_model.keras'

# Tarkista, onko tokenisaattorit tallennettu levylle
tokenizer_words_file = 'tokenizer_words.pickle'
tokenizer_sentences_file = 'tokenizer_sentences.pickle'

if os.path.exists(words_model_file) and os.path.exists(sentences_model_file):
# Kysy käyttäjältä halutaanko mallit ladata vai kouluttaa uudelleen
choice = input("Mallit ovat jo olemassa. Haluatko kouluttaa ne uudelleen? (Kyllä/Ei): ").lower()

if choice == 'ei':
# Ladataan tallennetut mallit
print("Ladataan tallennetut mallit...")
model_words = tf.keras.models.load_model(words_model_file)
model_sentences = tf.keras.models.load_model(sentences_model_file)
# Jos tokenisaattorit ovat tallennettu, ladataan ne
if os.path.exists(tokenizer_words_file) and os.path.exists(tokenizer_sentences_file):
print("Ladataan tallennetut tokenisaattorit...")
with open(tokenizer_words_file, 'rb') as handle:
tokenizer_words = pickle.load(handle)
with open(tokenizer_sentences_file, 'rb') as handle:
tokenizer_sentences = pickle.load(handle)
else:
print("Koulutetaan tokenisaattorit uudelleen...")
# Tokenizer muuntaa sanat sekvensseiksi (sanaluokkamalli)
tokenizer_words = tf.keras.preprocessing.text.Tokenizer()
tokenizer_words.fit_on_texts(all_words)

# Tokenizer pitkille lauseille (lausemalli)
tokenizer_sentences = tf.keras.preprocessing.text.Tokenizer()
tokenizer_sentences.fit_on_texts(long_sentences)

# Tallennetaan tokenisaattorit myöhempää käyttöä varten
with open(tokenizer_words_file, 'wb') as handle:
pickle.dump(tokenizer_words, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open(tokenizer_sentences_file, 'wb') as handle:
pickle.dump(tokenizer_sentences, handle, protocol=pickle.HIGHEST_PROTOCOL)

# Tokenizer muuntaa sanat sekvensseiksi (sanaluokkamalli)
tokenizer_words = tf.keras.preprocessing.text.Tokenizer()
tokenizer_words.fit_on_texts(all_words)

# Tokenizer pitkille lauseille (lausemalli)
tokenizer_sentences = tf.keras.preprocessing.text.Tokenizer()
tokenizer_sentences.fit_on_texts(long_sentences)

# Tallennetaan tokenisaattorit myöhempää käyttöä varten
with open(tokenizer_words_file, 'wb') as handle:
pickle.dump(tokenizer_words, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open(tokenizer_sentences_file, 'wb') as handle:
pickle.dump(tokenizer_sentences, handle, protocol=pickle.HIGHEST_PROTOCOL)

else:
# Koulutetaan mallit uudelleen
print("Koulutetaan mallit uudelleen...")
# Ensimmäinen malli: Sanaluokkamalli (Words Model)
model_words = tf.keras.Sequential([
layers.Embedding(input_dim=len(tokenizer_words.word_index) + 1, output_dim=64),
layers.LSTM(128, return_sequences=True),
layers.Dropout(0.5),
layers.Dense(len(tokenizer_words.word_index) + 1, activation='softmax')
])
model_words.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])

# Toinen malli: Lausemalli (Sentences Model)
model_sentences = tf.keras.Sequential([
layers.Embedding(input_dim=len(tokenizer_sentences.word_index) + 1, output_dim=64),
layers.LSTM(128, return_sequences=True),
layers.Dropout(0.5),
layers.Dense(len(tokenizer_sentences.word_index) + 1, activation='softmax')
])
model_sentences.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])

# Koulutetaan ensimmäinen malli (sanaluokkamalli)
print("Training Words Model")
model_words.fit(input_sequences_words, input_sequences_words, epochs=20)
model_words.save(words_model_file)

# Koulutetaan toinen malli (lausemalli)
print("Training Sentences Model")
model_sentences.fit(input_sequences_sentences, input_sequences_sentences, epochs=10)
model_sentences.save(sentences_model_file)

# Tokenizer muuntaa sanat sekvensseiksi (sanaluokkamalli)
tokenizer_words = tf.keras.preprocessing.text.Tokenizer()
tokenizer_words.fit_on_texts(all_words)

# Tokenizer pitkille lauseille (lausemalli)
tokenizer_sentences = tf.keras.preprocessing.text.Tokenizer()
tokenizer_sentences.fit_on_texts(long_sentences)

# Tallennetaan tokenisaattorit myöhempää käyttöä varten
with open(tokenizer_words_file, 'wb') as handle:
pickle.dump(tokenizer_words, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open(tokenizer_sentences_file, 'wb') as handle:
pickle.dump(tokenizer_sentences, handle, protocol=pickle.HIGHEST_PROTOCOL)

else:
# Jos mallia ei ole olemassa, koulutetaan se
print("Malleja ei löydy, koulutetaan ne uudelleen...")
# Ensimmäinen malli: Sanaluokkamalli (Words Model)

model_words = tf.keras.Sequential([
layers.Embedding(input_dim=len(tokenizer_words.word_index) + 1, output_dim=64),
layers.LSTM(128, return_sequences=True),
layers.Dropout(0.5),
layers.Dense(len(tokenizer_words.word_index) + 1, activation='softmax')
])
optimizer = tf.keras.optimizers.Adam(learning_rate=0.0005)
model_words.compile(optimizer=optimizer, loss='sparse_categorical_crossentropy', metrics=['accuracy'])
# model_words.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])

# Toinen malli: Lausemalli (Sentences Model)
model_sentences = tf.keras.Sequential([
layers.Embedding(input_dim=len(tokenizer_sentences.word_index) + 1, output_dim=64),
layers.LSTM(128, return_sequences=True),
layers.Dense(len(tokenizer_sentences.word_index) + 1, activation='softmax')
])
model_sentences.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])

# Koulutetaan ensimmäinen malli (sanaluokkamalli)
print("Training Words Model")
model_words.fit(input_sequences_words, input_sequences_words, epochs=10)
model_words.save(words_model_file)

# Koulutetaan toinen malli (lausemalli)
print("Training Sentences Model")
model_sentences.fit(input_sequences_sentences, input_sequences_sentences, epochs=10)
model_sentences.save(sentences_model_file)

# Tokenizer muuntaa sanat sekvensseiksi (sanaluokkamalli)
tokenizer_words = tf.keras.preprocessing.text.Tokenizer()
tokenizer_words.fit_on_texts(all_words)

# Tokenizer pitkille lauseille (lausemalli)
tokenizer_sentences = tf.keras.preprocessing.text.Tokenizer()
tokenizer_sentences.fit_on_texts(long_sentences)

# Tallennetaan tokenisaattorit myöhempää käyttöä varten
with open(tokenizer_words_file, 'wb') as handle:
pickle.dump(tokenizer_words, handle, protocol=pickle.HIGHEST_PROTOCOL)

with open(tokenizer_sentences_file, 'wb') as handle:
pickle.dump(tokenizer_sentences, handle, protocol=pickle.HIGHEST_PROTOCOL)


# Funktio embedding-kerroksen painojen tarkasteluun
def view_embeddings(model, tokenizer, word=None):
embedding_layer = model.layers[0]
embedding_weights = embedding_layer.get_weights()[0] # Haetaan painot

print(f"Embedding-kerroksen muoto: {embedding_weights.shape}")

if word:
word_index = tokenizer.word_index.get(word)
if word_index:
word_embedding = embedding_weights[word_index]
print(f"Embedding '{word}' sanojen osalta: {word_embedding}")
else:
print(f"Sanaa '{word}' ei löydy sanastosta.")
else:
# Tulostetaan muutama ensimmäinen embedding-paino selkeyden vuoksi
print("Näytetään ensimmäiset embedding-painot:")
print(embedding_weights[:5]) # Näytetään 5 ensimmäistä riviä


# Funktio mallin ennustusten tarkasteluun
def view_predictions(model, tokenizer, input_text, max_len=10):
input_sequence = tokenizer.texts_to_sequences([input_text])
input_sequence = tf.keras.preprocessing.sequence.pad_sequences(input_sequence, maxlen=max_len)

# Ennustetaan seuraava sana
prediction = model.predict(input_sequence)
predicted_word_index = np.argmax(prediction, axis=-1)

# Tarkistetaan, onko indeksiä sanastossa
predicted_word = tokenizer.index_word.get(predicted_word_index[0][0], "tuntematon")

if predicted_word == "tuntematon":
print(f"Ei ennustetta sanalle/lauseelle '{input_text}'.")
else:
print(f"Ennuste sanalle/lauseelle '{input_text}': {predicted_word}")


# Funktio LSTM-kerroksen painojen tarkasteluun
def view_lstm_weights(model):
lstm_layer = model.layers[1]
lstm_weights = lstm_layer.get_weights() # Tämä palauttaa listan, jossa on kolme osaa

print(f"LSTM-kerroksen syötepainot muoto: {lstm_weights[0].shape}")
print(f"LSTM-kerroksen tilapainot muoto: {lstm_weights[1].shape}")
print(f"LSTM-kerroksen vinoutumispainot muoto: {lstm_weights[2].shape}")

# Näytetään ensimmäiset arvot selkeyden vuoksi
print("Ensimmäiset syötepainot:", lstm_weights[0][:5])
print("Ensimmäiset tilapainot:", lstm_weights[1][:5])
print("Ensimmäiset vinoutumispainot:", lstm_weights[2][:5])


def find_closest_words(target_embedding, embedding_matrix, tokenizer, top_n=5):
# Lasketaan kosinietäisyydet
similarities = cosine_similarity(target_embedding.reshape(1, -1), embedding_matrix)[0]

# Haetaan top N lähimpää vektoria
closest_indices = np.argsort(similarities)[-top_n:][::-1]

# Haetaan sanat, jotka vastaavat lähimpiä indeksejä
closest_words = [tokenizer.index_word[i] for i in closest_indices if i in tokenizer.index_word]

return closest_words


def hae_embedding():
# Haetaan embedding-kerroksen painot model_words-mallista
embedding_layer = model_words.layers[0] # Oletetaan, että embedding-kerros on ensimmäinen kerros
embedding_weights_words = embedding_layer.get_weights()[0] # Haetaan painot

# Haetaan "iso"-embedding ja etsitään lähimmät sanat
iso_embedding = embedding_weights_words[tokenizer_words.word_index["iso"]]
closest_words = find_closest_words(iso_embedding, embedding_weights_words, tokenizer_words, top_n=5)

print(f"Sanat, jotka ovat lähimpänä sanaa 'iso': {closest_words}")

# Tarkistetaan, onko sana palindromi
def is_palindrome(s):
return s == s[::-1]


def predict_next_word(model, tokenizer, current_sequence):
input_sequence = tokenizer.texts_to_sequences([current_sequence])
input_sequence = tf.keras.preprocessing.sequence.pad_sequences(input_sequence, maxlen=10)

# Tulostetaan input-sekvenssi
print(f"Syötesekvenssi: {input_sequence}")

# Ennustetaan seuraava sana
predictions = model.predict(input_sequence)
predicted_index = np.argmax(predictions, axis=-1)[0][0]

predicted_word = tokenizer.index_word.get(predicted_index, None)
print(f"Ennustettu sana: {predicted_word}")

return predicted_word



# Rakennetaan palindromi hyödyntäen opetettua sanaluokkamallia
def build_palindrome_with_model(start_word, model, tokenizer):
current_palindrome = start_word
reverse_palindrome = start_word[::-1] # Käännetty versio, jota verrataan

for _ in range(10): # Yritetään lisätä enintään 10 sanaa
next_word = predict_next_word(model, tokenizer, current_palindrome)

if next_word:
# Yritetään lisätä sana alkuun
new_palindrome_start = next_word + " " + current_palindrome
if is_palindrome(new_palindrome_start):
current_palindrome = new_palindrome_start
print(f"Palindromi lisätty alkuun: {current_palindrome}")
continue

# Yritetään lisätä sana loppuun
new_palindrome_end = current_palindrome + " " + next_word
reverse_test = reverse_palindrome + next_word[::-1]
if is_palindrome(reverse_test): # Tarkistetaan symmetria
current_palindrome = new_palindrome_end
reverse_palindrome = reverse_test
print(f"Palindromi lisätty loppuun: {current_palindrome}")
else:
print("Ei ennustettua sanaa. Lopetetaan.")

return current_palindrome


# Testataan ennustuksia ja palindromin rakentamista
def tee_palindromi():
start_word = "iso"
final_palindrome = build_palindrome_with_model(start_word, model_words, tokenizer_words)
print(f"Valmis palindromi: {final_palindrome}")
# Tulosta joitakin tokenisoituja sanoja, esim. 'iso'
print(f"Sanan 'iso' indeksit: {tokenizer_words.texts_to_sequences(['iso'])}")


# Funktio valikon luomiseksi
def main_menu():
while True:
print("\nValitse toiminto:")
print("1: Tarkastele embedding-kerroksen painoja")
print("2: Tarkastele mallin ennustuksia")
print("3: Tarkastele LSTM-kerroksen painoja")
print("4: Lähimmät sanat lähellä sanaa iso")
print("5: Tee palindromi alkaen sanasta iso")
print("6: Poistu")
choice = input("Anna valintasi (1-6): ")

if choice == '1':
word = input("Anna sana (tai paina Enter, jos haluat tarkastella koko embedding-kerrosta): ")
if word:
view_embeddings(model_words, tokenizer_words, word=word)
else:
view_embeddings(model_words, tokenizer_words)
elif choice == '2':
input_text = input("Anna sana tai lause ennustusta varten: ")
view_predictions(model_words, tokenizer_words, input_text)
elif choice == '3':
view_lstm_weights(model_words)
elif choice == '4':
hae_embedding()
elif choice == '5':
tee_palindromi()
elif choice == '6':
print("Poistutaan.")
break
else:
print("Virheellinen valinta, yritä uudelleen.")


# Lataa mallit ja tokenizerit (oletetaan, että mallit on jo koulutettu ja tallennettu)
model_words = tf.keras.models.load_model('words_model.keras')
model_sentences = tf.keras.models.load_model('sentences_model.keras')

# Oletetaan, että tokenizer on myös valmiiksi koulutettu
# tokenizer_words ja tokenizer_sentences oletetaan olevan olemassa

# Käynnistetään valikko
main_menu()
Malli kyllästyi hyvin ja oli todella tarkka

Embedding-kerros vaikutti toimivan:

Embedding kerroksen painoja sanalle malli

Ennustukset eivät kuitenkaan toimineet laisinkaan. LTSM-vinoutumispainot sanalle iso olivat aina 512:

Sanan iso vinoutumispainot

Sanat, jotka ovat lähimpänä sanaa ‘iso’: [‘iso’, ‘kaskenpolttaja’, ‘implisoida’, ‘diplomilaulaja’, ‘kastemalja’] ja tämä tieto siis perustui opettamaan kirjatietoon. Palindromin teko sanasta ‘iso’ ei toiminut, sillä malli ei löytänyt sopivia sanoja, koska läheisyys sopiville sanoille puuttui.

Tee palindromi alkaen sanasta iso

Tämä johti ajatukseen siitä, että tarvittaisiin pickle.

import tensorflow as tf
from tensorflow.keras import layers
import numpy as np
import pickle
import os

# Ladataan aiemmin koulutetut sanaluokka- ja lausemallit
model_words = tf.keras.models.load_model('words_model.keras')
model_sentences = tf.keras.models.load_model('sentences_model.keras')

# Ladataan tokenisaattorit
with open('tokenizer_words.pickle', 'rb') as handle:
tokenizer_words = pickle.load(handle)
with open('tokenizer_sentences.pickle', 'rb') as handle:
tokenizer_sentences = pickle.load(handle)

# Tarkistetaan, onko sana palindromi
def is_palindrome(s):
return s == s[::-1]


# Tarkistetaan, onko sana palindromi
def is_palindrome(s):
return s == s[::-1]


# Ennustetaan seuraava sana sanaluokkamallin avulla ja tulostetaan kaikki vaiheet
def predict_next_word_words_model(current_sequence, model, tokenizer):
input_sequence = tokenizer.texts_to_sequences([current_sequence])
input_sequence = tf.keras.preprocessing.sequence.pad_sequences(input_sequence, maxlen=10)

print(f"Syötesekvenssi ennen padatointia: {input_sequence}")

predictions = model.predict(input_sequence)

# Tulostetaan koko ennustematriisi
print(f"Ennustematriisi: {predictions}")

# Etsitään suurin arvo ennustematriisista
predicted_index = np.argmax(predictions, axis=-1)[0][0]
predicted_word = tokenizer.index_word.get(predicted_index, None)

print(f"Ennustettu sana: {predicted_word}")
return predicted_word


# Rakennetaan palindromi käyttämällä vain sanaluokkamallia
def build_palindrome_with_words_model(start_word, model_words, tokenizer_words):
current_palindrome = start_word
reverse_palindrome = start_word[::-1]

for _ in range(10): # Yritetään lisätä enintään 10 sanaa
# Ennustetaan seuraava sana sanaluokkamallilla
next_word_words = predict_next_word_words_model(current_palindrome, model_words, tokenizer_words)

if next_word_words:
# Yritetään lisätä sana alkuun
new_palindrome_start = next_word_words + " " + current_palindrome
if is_palindrome(new_palindrome_start):
current_palindrome = new_palindrome_start
print(f"Palindromi lisätty alkuun: {current_palindrome}")
continue

# Yritetään lisätä sana loppuun
new_palindrome_end = current_palindrome + " " + next_word_words
reverse_test = reverse_palindrome + next_word_words[::-1]
if is_palindrome(reverse_test): # Tarkistetaan symmetria
current_palindrome = new_palindrome_end
reverse_palindrome = reverse_test
print(f"Palindromi lisätty loppuun: {current_palindrome}")
else:
print("Ei ennustettua sanaa. Lopetetaan.")
break

return current_palindrome


# Testataan palindromin rakentamista sanaluokkamallin avulla
def tee_palindromi():
start_word = "iso"
final_palindrome = build_palindrome_with_words_model(start_word, model_words, tokenizer_words)
print(f"Valmis palindromi: {final_palindrome}")


# Käynnistetään palindromin rakentaminen
print(f"'iso' tokenisaattorissa: {tokenizer_words.word_index.get('iso')}")
# Tarkistetaan, onko "iso" osa tokenisaattoria
iso_index = tokenizer_words.word_index.get('iso', None)
if iso_index:
print(f"Sanalle 'iso' löytyy indeksi: {iso_index}")
else:
print("Sanaa 'iso' ei löydy tokenisaattorista.")
tee_palindromi()
Palindromin luontiyritys hyödyntämällä pickleä

Ongelmaksi muodostui myös se, että jos ennustetaan sanaa, yhden kirjaimen muutos sanassa jää tunnistamatta, vaikka tarkkuudeksi laittaisi 0.95. Kirjaimien osalta tilanne on sama:

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import numpy as np

import csv
import re

# Ladataan sanat CSV-tiedostoista
def load_words(file_name):
with open(file_name, newline='') as f:
reader = csv.reader(f)
words = [row[0] for row in reader if row] # Valitaan vain sana, ei tyhjiä rivejä
return words

# Ladataan rivit CSV-tiedostoista
def load_text_rows(file_name):
with open(file_name, "r") as file: # comma separated values
text_rows = list(csv.reader(file, delimiter=","))
# Puristetaan listasta listojen sisältö, jotta ei ole sisäkkäisiä listoja
text_rows = [item for sublist in text_rows for item in sublist if item]
return text_rows

# Ladataan lauseet tavallisesta tekstitiedostosta
def load_sentences(file_name):
with open(file_name, 'r', encoding='utf-8') as f:
sentences = f.readlines() # Lue rivit listaksi
sentences = [line.strip() for line in sentences if line.strip()] # Poistetaan tyhjät rivit
return sentences

def clean_text(text):
# Poistetaan kaikki paitsi aakkoset
text = re.sub(r'[^a-zA-ZäöåÄÖÅ]', '', text)
return text.lower() # Muutetaan myös kaikki kirjaimet pieniksi

# Duplikaattien poisto säilyttäen alkuperäinen järjestys
def remove_duplicates(input_list):
seen = set() # Set muistaa jo havaitut elementit
unique_list = []
for item in input_list:
if item not in seen:
unique_list.append(item)
seen.add(item)
return unique_list

def is_palindrome(text):
return text == text[::-1]

# Funktio joka etsii pisimmät sanat annetuista listoista palindromin sisältä
def find_longest_words(palindrome, words):
found_words = []
i = 0
while i < len(palindrome):
longest_word = ""
for word in words:
if palindrome.startswith(word, i): # Etsitään kohta jossa sana alkaa
if len(word) > len(longest_word): # Valitaan pisin mahdollinen sana
longest_word = word

if longest_word:
found_words.append(longest_word)
i += len(longest_word) # Siirrytään eteenpäin pisimmän sanan verran
else:
i += 1 # Jos ei löydy sopivaa sanaa, siirrytään yksi askel eteenpäin

return found_words

# Haetaan tiedot
verbs = load_words('verbi_sanat.csv') # Lue csv sanat
adjectives = load_words('adjektiivi_sanat.csv') # Lue csv sanat
substantives = load_words('substantiivi_sanat.csv') # Lue csv sanat
palindromes = load_text_rows('palindromeja.csv') # Lue csv rivit
long_sentences = load_sentences('LongText.txt') # Lue lauseet

# Puhdistetaan tiedot
clean_verbs = remove_duplicates([clean_text(x) for x in verbs])
clean_adjectives = remove_duplicates([clean_text(x) for x in adjectives])
clean_substantives = remove_duplicates([clean_text(x) for x in substantives])
clean_palindromes = remove_duplicates([clean_text(x) for x in palindromes])
clean_long_sentences = remove_duplicates(clean_text(x) for x in long_sentences)

# Sanat ja palindromit eriteltyinä
verb_palindromes = []
verb_non_palindromes = []
adjective_palindromes = []
adjective_non_palindromes = []
substantive_palindromes = []
substantive_non_palindromes = []
palindromes_list = []
non_palindromes = []
long_sentence_palindromes = []
long_sentence_non_palindromes = []

# Tarkistetaan onko palindromi vai ei, jaotellaan sen mukaisesti
for x in clean_verbs:
if is_palindrome(x):
verb_palindromes.append(x)
else:
verb_non_palindromes.append(x)

for x in clean_adjectives:
if is_palindrome(x):
adjective_palindromes.append(x)
else:
adjective_non_palindromes.append(x)

for x in clean_substantives:
if is_palindrome(x):
substantive_palindromes.append(x)
else:
substantive_non_palindromes.append(x)

for x in clean_palindromes:
if is_palindrome(x):
palindromes_list.append(x)
else:
non_palindromes.append(x)

for x in clean_long_sentences:
if is_palindrome(x):
long_sentence_palindromes.append(x)
else:
long_sentence_non_palindromes.append(x)

# Testataan tunnetulla palindromilla, esim. "saippuakauppias"
# test_palindrome = "AatosukkiPauhuakauhuasauhuapauhuakauhuapauhuasauhuakauhuaPikkusotaa"
# found_words = find_longest_words(test_palindrome, clean_verbs + clean_adjectives + clean_substantives)

# Tulostetaan löydetyt sanat
# print(f"Löydetyt sanat palindromista '{test_palindrome}': {found_words}")

# ********** Generontiosuus ************
palindrome_sentences = long_sentence_palindromes
non_palindrome_sentences = non_palindromes + long_sentence_non_palindromes

# Yhdistetään palindromit ja ei-palindromit, sekä annetaan niille luokkamerkit
sentences = palindrome_sentences + non_palindrome_sentences
labels = [1] * len(palindrome_sentences) + [0] * len(non_palindrome_sentences) # 1 = palindromi, 0 = ei-palindromi

# Tokenisoidaan lauseet
tokenizer = Tokenizer(char_level=False, lower=True)
tokenizer.fit_on_texts(sentences)
sequences = tokenizer.texts_to_sequences(sentences)

# Padataan sekvenssit samanpituisiksi (lisätään nollia lyhyiden sekvenssien loppuun)
max_sequence_len = max([len(seq) for seq in sequences])
padded_sequences = pad_sequences(sequences, maxlen=max_sequence_len, padding='post')

# Muutetaan lista numpy-taulukoksi
X = np.array(padded_sequences)
y = np.array(labels)

# Jaetaan data opetus- ja testiaineistoon
split = int(0.8 * len(X))
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

# Rakennetaan LSTM-malli
model = tf.keras.Sequential([
layers.Embedding(input_dim=len(tokenizer.word_index) + 1, output_dim=64, input_length=max_sequence_len),
layers.LSTM(128),
layers.Dense(64, activation='relu'),
layers.Dense(1, activation='sigmoid') # sigmoid aktivointi, koska kyseessä on binääriluokittelu
])

# Mallin kompiloiminen
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

# Koulutetaan malli
model.fit(X_train, y_train, epochs=10, validation_data=(X_test, y_test))

# Testataan malli uudella lauseella
new_sentence = "Nootti hassu sanoi Loat Saasta pese patsaasta olion asussa hittoon" # Voisiko tämä olla palindromi?
new_sequence = tokenizer.texts_to_sequences([new_sentence])
new_padded_sequence = pad_sequences(new_sequence, maxlen=max_sequence_len, padding='post')

# Ennustetaan onko palindromi vai ei
prediction = model.predict(new_padded_sequence)
if prediction > 0.95:
print(f"Lause '{new_sentence}' on palindromi")
else:
print(f"Lause '{new_sentence}' ei ole palindromi")

Vaikka if prediction > 0.95 (tässä siis merkkipohjaisesti), ei palindromia tunnistettu:

Palindromia ei tunnistettu

Pienemmillä todennäköisyydellä toki tunnistetaan palindromi, mutta yhden kirjaimen muutos jää tunnistamatta.

Tässä vaiheessa päätin antaa periksi, sillä aika ja mielenkiinto testaamiseen loppui. Käytin aikaa testaamiseen noin 5 tuntia. Ehkä myös taito hyväksikäyttää Keras ja Tensorflow- kirjastoja.

Sen verran ongelma ärsytti, että tein päättelylogiikan ja toimiva koodi ja logiikka ainakin sanalle saippuakauppias löytyy tästä artikkelista.

--

--

Jari Hiltunen

With 20+ years in ICT, innovation in AI & human sciences. A lifelong learner, open-source advocate, empathetic supporter of personal & professional grower.