1
Fork 0
mirror of https://github.com/Jaxan/monoid-learner.git synced 2025-04-26 22:47:45 +02:00

Working implementation

This commit is contained in:
Joshua Moerman 2021-03-12 09:51:53 +01:00
parent 0e70561e4f
commit 1faca5b575
7 changed files with 277 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
dist-newstyle
*.code-workspace

View file

@ -1,3 +1,8 @@
monoid-learner
==============
Learns the minimal monoid accepting an unknown language through an orcale.
Similar to Lstar, but for monoids instead of automata. The output is a monoid
representation which is furthermore minimised by the Knuth-Bendix completion.
[Original](https://gist.github.com/Jaxan/d9bb9e3223e8fe8266fe4fe84d357088)

2
Setup.hs Normal file
View file

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

41
app/Main.hs Normal file
View file

@ -0,0 +1,41 @@
module Main where
import qualified Data.Map as Map
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import KnuthBendix
import MStar
-- We use the alphabet {a, b} as always
alphabet :: Set.Set Char
alphabet = Set.fromList "ab"
-- Example language L = { w | nonempty && even number of as && triple numbers of bs }
language :: MStar.Word Char -> Bool
language w = not (null w) && length aa `mod` 2 == 0 && length bb `mod` 3 == 0
where
(aa, bb) = Seq.partition (== 'a') w
main :: IO ()
main = do
let -- Initialise
s = initialState alphabet language
-- make closed, consistent and associative
(_, s2) = learn language s
-- The corresponding hypothesis is wrong on the string bbb
-- So we add a row bb
s3 = addRow (Seq.fromList "bb") language s2
-- Make closed, consistent and associative again
(_, s4) = learn language s3
-- Extract the rewrite rules from the table
-- For this we simply look at products r1 r2 and see which row is equivalent to it
rowPairs = Set.filter (\w -> not (w `Set.member` rows s4)) . Set.map (uncurry (<>)) $ Set.cartesianProduct (rows s4) (rows s4)
representatives = Map.fromList (fmap (\w -> (row s4 w, w)) (Set.toList (rows s4)))
rules0 = Map.fromSet (\w -> representatives Map.! row s4 w) rowPairs
rules = Map.toList rules0
putStrLn "Inferred rules: (generators are a, b and the unit)"
print rules
putStrLn "After KB:"
print (knuthBendix rules)

27
monoid-learner.cabal Normal file
View file

@ -0,0 +1,27 @@
cabal-version: 2.2
name: monoid-learner
version: 0.1.0.0
author: Joshua Moerman
maintainer: joshua@cs.rwth-aachen.de
build-type: Simple
common stuff
default-language: Haskell2010
ghc-options: -Wall -O2
build-depends:
base >=4.12 && <5,
containers,
HaskellForMaths >=0.4
library
import: stuff
hs-source-dirs: src
exposed-modules:
KnuthBendix,
MStar
executable monoid-learner
import: stuff
hs-source-dirs: app
main-is: Main.hs
build-depends: monoid-learner

9
src/KnuthBendix.hs Normal file
View file

@ -0,0 +1,9 @@
module KnuthBendix where
import Data.Sequence (Seq, fromList)
import Data.Foldable (Foldable (toList))
import Data.Bifunctor (Bifunctor (bimap))
import qualified Math.Algebra.Group.StringRewriting as Rew
knuthBendix :: Ord a => [(Seq a, Seq a)] -> [(Seq a, Seq a)]
knuthBendix = fmap (bimap fromList fromList) . Rew.knuthBendix . fmap (bimap toList toList)

191
src/MStar.hs Normal file
View file

@ -0,0 +1,191 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
module MStar where
-- Copyright Joshua Moerman 2020
-- M*: an algorithm to query learn the syntactic monoid
-- for a regular language. I hope it works correctly.
-- This is a rough sketch, and definitely not cleaned up.
import Control.Applicative (Applicative ((<*>)), (<$>))
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Sequence (Seq, empty, singleton)
import Data.Set (Set)
import qualified Data.Set as Set
import Prelude hiding (Word)
type Word a = Seq a
type Alphabet a = Set a
type MembershipQuery a = Word a -> Bool
-- If l includes the empty word, then this set also includes l
squares :: _ => Set (Word a) -> Set (Word a)
squares l = Set.map (uncurry (<>)) (Set.cartesianProduct l l)
-- Left and Right concats, these are like columns, but they act
-- both left and right. Maybe a better word would be "tests".
type Context a = (Word a, Word a)
type Contexts a = Set (Context a)
initialContexts :: Contexts a
initialContexts = Set.singleton (empty, empty)
type Row a = Word a
type Rows a = Set (Row a)
initialRows :: Ord a => Alphabet a -> Rows a
initialRows alphabet = Set.singleton empty `Set.union` Set.map singleton alphabet
-- State of the M* algorithm
data State a = State
{ rows :: Rows a,
contexts :: Contexts a,
cache :: Map (Word a) Bool
}
deriving (Show, Eq)
-- Row data for an index
row :: _ => State a -> Row a -> Map (Context a) Bool
row State {..} m = Map.fromSet (\(l, r) -> cache Map.! (l <> m <> r)) contexts
-- Difference of two rows (i.e., all contexts in which they differ)
difference :: _ => State a -> Row a -> Row a -> [Context a]
difference State {..} m1 m2 = [(l, r) | (l, r) <- Set.toList contexts, cache Map.! (l <> m1 <> r) /= cache Map.! (l <> m2 <> r)]
-- Initial state of the algorithm
initialState :: Ord a => Alphabet a -> MembershipQuery a -> State a
initialState alphabet mq =
State
{ rows = rows,
contexts = contexts,
cache = cache
}
where
rows = initialRows alphabet
contexts = initialContexts
initialQueries =
Set.map (\(m, (l, r)) -> l <> m <> r) $
Set.cartesianProduct (squares rows) contexts
cache = Map.fromSet mq initialQueries
-- CLOSED --
-- Returns all pairs which are not closed
closed :: _ => State a -> [(Row a, Row a)]
closed s@State {..} = [(m1, m2) | (m1, m2) <- Set.toList rowPairs, notExists (row s (m1 <> m2))]
where
rowPairs = Set.cartesianProduct rows rows
allRows = Set.map (row s) rows
notExists m = not (m `Set.member` allRows)
-- Returns a fix for the non-closedness.
fixClosed :: _ => _ -> Maybe (Word a)
fixClosed [] = Nothing
fixClosed ((a, b) : _) = Just (a <> b)
-- Adds a new element
addRow :: Ord a => Row a -> MembershipQuery a -> State a -> State a
addRow m mq s@State {..} = s {rows = newRows, cache = cache <> dCache}
where
newRows = Set.insert m rows
queries = Set.map (\(mi, (l, r)) -> l <> mi <> r) $ Set.cartesianProduct (squares newRows) contexts
queriesRed = queries `Set.difference` Map.keysSet cache
dCache = Map.fromSet mq queriesRed
-- CONSISTENT --
-- Returns all inconsistencies
consistent :: _ => State a -> _
consistent s@State {..} = [(m1, m2, n1, n2, d) | (m1, m2) <- equalRowPairs, (n1, n2) <- equalRowPairs, let d = difference s (m1 <> n1) (m2 <> n2), not (Prelude.null d)]
where
equalRowPairs = Prelude.filter (\(m1, m2) -> row s m1 == row s m2) $ (,) <$> Set.toList rows <*> Set.toList rows
-- Returns a fix for consistency.
fixConsistent :: _ => State a -> _ -> Maybe (Context a)
fixConsistent _ [] = Nothing
fixConsistent _ ((_, _, _, _, []) : _) = error "Cannot happen cons"
fixConsistent s ((m1, m2, n1, n2, (l, r) : _) : _) = Just . head . Prelude.filter valid $ [(l <> m1, r), (l <> m2, r), (l, n1 <> r), (l, n2 <> r)] -- Many choices here
where
valid c = not (Set.member c (contexts s))
-- Adds a test
addContext :: Ord a => Context a -> MembershipQuery a -> State a -> State a
addContext lr mq s@State {..} = s {contexts = newContexts, cache = cache <> dCache}
where
newContexts = Set.insert lr contexts
queries = Set.map (\(m, (l, r)) -> l <> m <> r) $ Set.cartesianProduct (squares rows) newContexts
queriesRed = queries `Set.difference` Map.keysSet cache
dCache = Map.fromSet mq queriesRed
-- ASSOCIATIVITY --
-- Returns non-associativity results. Implemented in a brute force way
-- This is something new in M*, it's obviously not needed in L*
associative :: _ => State a -> _
associative s@State {..} = [(m1, m2, m3, m12, m23, d) | (m1, m2, m3, m12, m23) <- allCandidates, let d = difference s (m12 <> m3) (m1 <> m23), not (Prelude.null d)]
where
rs = Set.toList rows
allTriples = [(m1, m2, m3) | m1 <- rs, m2 <- rs, m3 <- rs]
allCandidates = [(m1, m2, m3, m12, m23) | (m1, m2, m3) <- allTriples, m12 <- rs, row s m12 == row s (m1 <> m2), m23 <- rs, row s m23 == row s (m2 <> m3)]
-- Fix for associativity (hopefully)
fixAssociative :: _ => _ -> Maybe (Context a)
fixAssociative [] = Nothing
fixAssociative ((_, _, _, _, _, []) : _) = error "Cannot happen assoc"
fixAssociative ((_, _, m3, _, _, (l, r) : _) : _) = Just (l, m3 <> r) -- TODO: many choices
-- Abstract data type for a monoid. The map from the alphabet
-- determines the homomorphism from Words to this monoid
data MonoidAcceptor a q = MonoidAcceptor
{ elements :: [q], -- set of elements
unit :: q, -- the unit element
multiplication :: q -> q -> q, -- multiplication functions
accept :: q -> Bool, -- accepting subset
alph :: a -> q -- map from alphabet
}
-- Given a word, is it accepted by the monoid?
acceptMonoid :: MonoidAcceptor a q -> Word a -> Bool
acceptMonoid MonoidAcceptor {..} w = accept (foldr multiplication unit (fmap alph w))
-- HYPOTHESIS --
-- Syntactic monoid construction
constructMonoid :: _ => State a -> MonoidAcceptor a Int
constructMonoid s@State {..} =
MonoidAcceptor
{ elements = [0 .. Set.size allRows - 1],
unit = unit,
multiplication = curry (multMap Map.!),
accept = (accMap Map.!),
alph = rowToInt . singleton
}
where
allRows = Set.map (row s) rows
rowMap = Map.fromList (zip (Set.toList allRows) [0 ..])
rowToInt m = rowMap Map.! row s m
unit = rowToInt empty
accMap = Map.fromList [(rowMap Map.! r, r Map.! (empty, empty)) | r <- Set.toList allRows]
multList = [((rowToInt m1, rowToInt m2), rowToInt (m1 <> m2)) | m1 <- Set.toList rows, m2 <- Set.toList rows]
multMap = Map.fromList multList
-- Learns until it can construct a monoid
-- Please do counterexample handling yourself
learn :: _ => MembershipQuery a -> State a -> (MonoidAcceptor a _, State a)
learn mq s =
case fixClosed $ closed s of
Just m -> learn mq (addRow m mq s)
Nothing ->
case fixConsistent s $ consistent s of
Just c -> learn mq (addContext c mq s)
Nothing ->
case fixAssociative $ associative s of
Just c -> learn mq (addContext c mq s)
Nothing -> (constructMonoid s, s)