Кодоход: Генерация произвольного текста: алгоритм Маркова
Поэтому в большинстве нашего кода мы будем моделировать префиксы как
[]string и объединять строки вместе с помощью пробела,
чтобы сгенерировать ключ карты:
Prefix Map key
[]string{"", ""} " "
[]string{"", "I"} " I"
[]string{"I", "am"} "I am"
doc/codewalk/markov.go:77
Структура Chain
Полное состояние таблицы цепочек состоит из самой таблицы и
длины слов префиксов. Структура
Chain хранит
эти данные.
doc/codewalk/markov.go:76,79
Функция-конструктор NewChain
Структура
Эта функция-конструктор не является строго необходимой, поскольку вся программа находится в одном пакете (
Chain имеет два неэкспортируемых поля (те,
которые не начинаются с заглавной буквы), поэтому мы пишем
функцию-конструктор NewChain, которая инициализирует
карту chain с помощью make и устанавливает
поле prefixLen.
Эта функция-конструктор не является строго необходимой, поскольку вся программа находится в одном пакете (
main), и, следовательно,
практически нет разницы между экспортируемыми и неэкспортируемыми
полями. Можно было бы просто написать содержимое этой функции
при необходимости создания новой цепочки.
Однако использование этих неэкспортируемых полей является хорошей практикой;
это ясно показывает, что только методы типа Chain и его функция-конструктор
должны обращаться к этим полям. Кроме того, структурирование Chain
в таком виде позволяет легко переместить его в собственный пакет в будущем.
doc/codewalk/markov.go:82,84
Тип Prefix
Поскольку мы часто будем работать с префиксами, мы определим
тип
Prefix с конкретным типом []string.
Определение именованного типа позволяет нам быть явными, когда мы
работаем с префиксом вместо просто []string.
Кроме того, в Go можно определять методы для любого именованного типа
(не только для структур), поэтому мы можем добавить методы, работающие
с Prefix, если это понадобится.
doc/codewalk/markov.go:60
Метод String
Первый метод, который мы определяем на
Prefix, это
String. Он возвращает string-представление
Prefix, объединяя элементы среза с помощью пробелов.
Мы будем использовать этот метод для генерации ключей при работе с
цепной картой.
doc/codewalk/markov.go:63,65
Построение цепи
Метод
Метод
Build читает текст из io.Reader
и разбирает его на префиксы и суффиксы, которые хранятся в
Chain.
io.Reader — это
интерфейсный тип, который широко используется стандартной библиотекой
и другим Go-кодом. Наш код использует функцию
fmt.Fscan, которая
читает пробельно-разделённые значения из io.Reader.
Метод
Build завершает работу, когда метод Read
объекта Reader возвращает io.EOF (конец файла)
или другую ошибку чтения.
doc/codewalk/markov.go:88,100
Буферизация входных данных
Эта функция выполняет множество мелких чтений, что может быть неэффективно
для некоторых
Readers. Для повышения эффективности мы оборачиваем
предоставленный io.Reader с помощью
bufio.NewReader, чтобы создать новый
io.Reader, который обеспечивает буферизацию.
doc/codewalk/markov.go:89
Переменная Prefix
В начале функции мы создаём срез
Prefix
p, используя поле prefixLen объекта Chain
в качестве его длины.
Мы будем использовать эту переменную для хранения текущего префикса и изменять её
с каждым новым словом, которое встречаем.
doc/codewalk/markov.go:90
Сканирование слов
В цикле мы читаем слова из
Reader
fmt.Fscan возвращает ошибку, если она сталкивается с ошибкой чтения (io.EOF, например), или если не может считать запрашиваемое значение (в нашем случае — одиночную строку). В любом из этих случаев мы просто хотим остановить сканирование, поэтому используем break для выхода из цикла.
doc/codewalk/markov.go:92,95
Добавление префикса и суффикса в цепочку
Слово, хранимое в
Встроенная функция
Для получения дополнительной информации о функции
s, является новым суффиксом. Мы добавляем новую комбинацию префикс/суффикс в карту chain, вычисляя ключ карты с помощью p.String и добавляя суффикс в срез, хранящийся под этим ключом.
Встроенная функция
append добавляет элементы в срез и выделяет новое хранилище при необходимости. Когда предоставленный срез равен nil, append выделяет новый срез.
Это поведение удобно сочетается с семантикой нашей карты: получение неустановленного ключа возвращает нулевое значение типа значения, и нулевое значение []string равно nil.
Когда наша программа встречает новый префикс (что приводит к значению nil в карте), append выделит новый срез.
Для получения дополнительной информации о функции
append и срезах в целом см. статью
Slices: usage and internals.
doc/codewalk/markov.go:96,97
Добавление суффикса к префиксу
Прежде чем читать следующее слово, алгоритму требуется удалить первое слово из префикса и добавить текущий суффикс к префиксу.
В этом состоянии
В этом состоянии
p == Prefix{"I", "am"}
s == "not"
новое значение p будет
p == Prefix{"am", "not"}
Эта операция также требуется во время генерации текста, поэтому мы размещаем код, выполняющий это изменение среза, внутри метода типа Prefix под названием Shift.
doc/codewalk/markov.go:98
Метод Shift
Метод
Shift использует встроенную функцию copy для копирования последних len(p)-1 элементов p в начало среза, эффективно перемещая элементы на один индекс влево (если считать ноль как самый левый индекс).
p := Prefix{"I", "am"}
copy(p, p[1:])
// p == Prefix{"am", "am"}
Затем присваивается предоставленное значение word последнему индексу среза:
// suffix == "not"
p[len(p)-1] = suffix
// p == Prefix{"am", "not"}
doc/codewalk/markov.go:68,71
Генерация текста
Метод
Generate похож на Build, за исключением того, что вместо чтения слов из Reader и хранения их в карте, он читает слова из карты и добавляет их в срез (words).
Generate использует условный цикл for для генерации до n слов.
doc/codewalk/markov.go:103,116
Получение потенциальных суффиксов
На каждой итерации цикла мы получаем список потенциальных суффиксов для текущего префикса. Мы обращаемся к карте
Если
chain по ключу p.String() и присваиваем её содержимое переменной choices.
Если
len(choices) равен нулю, мы выходим из цикла, поскольку для этого префикса нет потенциальных суффиксов.
Эта проверка также работает, если ключ вообще отсутствует в карте: в этом случае choices будет nil, а длина nil среза равна нулю.
doc/codewalk/markov.go:107,110
Случайный выбор суффикса
Для выбора суффикса мы используем функцию
Мы используем этот индекс для выбора нового суффикса, присваиваем его переменной
Далее мы вызываем метод
rand.Intn.
Она возвращает случайное целое число от нуля (включительно) до заданного значения (исключая его). Передача len(choices) даёт нам случайный индекс в пределах длины списка.
Мы используем этот индекс для выбора нового суффикса, присваиваем его переменной
next и добавляем в срез words.
Далее мы вызываем метод
Shift с новым суффиксом, аналогично тому, как это делалось в методе Build.
doc/codewalk/markov.go:111,113
Возврат сгенерированного текста
Перед возвратом сгенерированного текста в виде строки мы используем
функцию
strings.Join для объединения элементов
среза words, разделённых пробелами.
doc/codewalk/markov.go:115
Флаги командной строки
Чтобы было удобно настраивать длину префикса и длину генерируемого текста,
мы используем пакет
Вызовы
flag для разбора
флагов командной строки.
Вызовы
flag.Int регистрируют новые флаги в пакете
flag. Аргументы функции Int — это имя флага,
его значение по умолчанию и описание. Функция Int возвращает
указатель на целое число, которое будет содержать значение, указанное
пользователем (или значение по умолчанию, если флаг не был указан в командной строке).
doc/codewalk/markov.go:119,121
Настройка программы
Функция
Если флаги командной строки, предоставленные пользователем, являются некорректными, функция
main начинается с разбора флагов командной строки
с помощью flag.Parse и инициализации генератора случайных чисел
пакета rand текущим временем.
Если флаги командной строки, предоставленные пользователем, являются некорректными, функция
flag.Parse выведет информативное
сообщение об использовании и завершит работу программы.
doc/codewalk/markov.go:123,124
Создание и построение нового Chain
Чтобы создать новый
Чтобы построить цепочку, мы вызываем
Chain, мы вызываем NewChain
с значением флага prefix.
Чтобы построить цепочку, мы вызываем
Build с
os.Stdin (который реализует io.Reader), чтобы
он читал входные данные из стандартного ввода.
doc/codewalk/markov.go:126,127
Генерация текста
После построения цепочки мы вызываем метод
Generate с
заданной длиной генерируемого текста. Этот метод возвращает сгенерированный
текст в виде строки.
doc/codewalk/markov.go:128,129
Генерация и вывод текста
Наконец, для генерации текста вызывается функция
Затем вызывается
Generate с
значением флага words и присваивается результат
переменной text.
Затем вызывается
fmt.Println для записи текста в стандартный
поток вывода, за которым следует возврат каретки.
doc/codewalk/markov.go:128,129
Использование этой программы
Чтобы использовать эту программу, сначала соберите её с помощью
команды go:
$ go build markov.goА затем выполните её, передав входной текст через конвейер:
$ echo "a man a plan a canal panama" \ | ./markov -prefix=1 a plan a man a plan a canal panamaВот транскрипт генерации текста с использованием файла README из дистрибутива Go в качестве исходного материала:
$ ./markov -words=10 < $GOROOT/README This is the source code repository for the Go source $ ./markov -prefix=1 -words=10 < $GOROOT/README This is the go directory (the one containing this README). $ ./markov -prefix=1 -words=10 < $GOROOT/README This is the variable if you have just untarred a
doc/codewalk/markov.go
Упражнение для читателя
Функция
Generate выполняет множество выделений памяти при
построении среза words. В качестве упражнения измените её,
чтобы она принимала io.Writer, в который постепенно записывает
сгенерированный текст с помощью Fprint.
Помимо повышения эффективности, это делает функцию Generate
более симметричной по отношению к функции Build.
doc/codewalk/markov.go
map[string][]string. Каждый ключ карты — это префикс (string), а его значения — это списки суффиксов (срез строк,[]string).Вот пример таблицы из комментария к пакету, представленный в виде этой структуры данных:
map[string][]string{ " ": {"I"}, " I": {"am"}, "I am": {"a", "not"}, "a free": {"man!"}, "am a": {"free"}, "am not": {"a"}, "a number!": {"I"}, "number! I": {"am"}, "not a": {"number!"}, }Хотя каждый префикс состоит из нескольких слов, мы храним префиксы в карте как одноstring. Следовало бы хранить префикс как[]string, но мы не можем сделать это с картой, потому что тип ключа карты должен поддерживать операцию равенства (а срезы такую операцию не поддерживают).