Codewalk: Generating arbitrary text: a Markov chain algorithm

Pop Out Code
左侧右侧的代码 码宽70% filepaths shownhidden
Introduction
此代码步描述了一个使用Markov链算法生成随机文本的程序. 程序包注释描述了算法和程序的操作. 请先阅读它,然后再继续.
doc/codewalk/markov.go:6,44
建模马尔可夫链
链由前缀和后缀组成. 每个前缀是固定数量的单词,而后缀是单个单词. 前缀可以有任意数量的后缀. 为了对此数据建模,我们使用map[string][]string . 每个映射键都是一个前缀( string ),其值是后缀列表(字符串的一部分, []string ).

这是此数据结构建模的包注释中的示例表:
  map [string] [] string {
	 " ": {"一世"},
	 " 我是"},
	 "我是":{" a"," not"},
	 "免费":{" man!"},
	 " am a":{"免费"},
	 "不是":{" a"},
	 "一个数字!":{" I"},
	 " number!I":{" am"},
	 " not a":{"数字!"},
 } 
虽然每个前缀都包含多个单词,但是我们将前缀作为单个string存储在映射中. 将前缀存储为[]string似乎更自然,但是我们不能用map来做到这一点,因为map的键类型必须实现相等(而slice则不需要).

因此,在我们的大多数代码中,我们将前缀建模为[]string并将这些字符串与空格连接在一起以生成map密钥:
  前缀映射键

 [] string {"",""}""
 [] string {"""," I"}" I"
 [] string {" I"," am"}"我是"
doc/codewalk/markov.go:77
链结构
链表的完整状态由表本身和前缀的字长组成. Chain结构存储此数据.
doc/codewalk/markov.go:76,79
The NewChain constructor function
Chain结构具有两个未导出的字段(不以大写字母开头的字段),因此我们编写了一个NewChain构造函数,该函数使用make初始化chain图并设置prefixLen字段.

由于整个程序都在单个包( main )中,因此构造函数并不是严格必需的,因此导出和未导出字段之间几乎没有实际区别. 当我们要构造一个新的Chain时,我们可以轻松地写出此函数的内容. 但是,使用这些未导出的字段是一个好习惯; 它清楚地表明,只有Chain方法及其构造函数才能访问这些字段. 同样,像这样构造Chain意味着我们可以在以后的某个日期轻松将其移入其自己的程序包中.
doc/codewalk/markov.go:82,84
前缀类型
由于我们经常使用前缀,因此我们使用具体类型[]string定义Prefix类型. 明确定义命名类型可以使我们在使用前缀而不是[]string时变得明确. 另外,在Go中,我们可以定义任何命名类型(而不仅仅是结构)的方法,因此,如果需要,我们可以添加对Prefix进行操作的方法.
doc/codewalk/markov.go:60
字符串方法
我们在Prefix上定义的第一个方法是String . 通过将slice元素与空格连接在一起,它返回Prefixstring表示形式. 在处理链图时,我们将使用此方法来生成密钥.
doc/codewalk/markov.go:63,65
建立链
Build方法从io.Reader读取文本,并将其解析为存储在Chain中的前缀和后缀.

io.Reader是标准库和其他Go代码广泛使用的接口类型. 我们的代码使用fmt.Fscan函数,该函数从io.Reader读取以空格分隔的值.

一旦ReaderRead方法返回io.EOF (文件末尾)或发生其他一些读取错误, Build方法即返回.
doc/codewalk/markov.go:88,100
缓冲输入
此功能执行许多次读取,对于某些Readers可能效率低下. 为了提高效率,我们将提供的io.Readerbufio.NewReader进行包装,以创建一个提供缓冲的新io.Reader .
doc/codewalk/markov.go:89
前缀变量
在函数的顶部,我们使用ChainprefixLen字段作为其长度来制作一个Prefix slice p . 我们将使用此变量来保存当前的前缀,并对遇到的每个新单词进行更改.
doc/codewalk/markov.go:90
扫描单词
在我们的循环中,我们使用fmt.FscanReader单词Readerstring变量s中. 由于Fscan使用空格分隔每个输入值,因此每个调用只会产生一个单词(包括标点符号),这正是我们所需要的.

如果Fscan遇到读取错误(例如io.EOF )或无法扫描请求的值(在我们的情况下为单个字符串),则返回错误. 无论哪种情况,我们都只想停止扫描,因此我们break了循环.
doc/codewalk/markov.go:92,95
在链中添加前缀和后缀
s存储的单词是一个新的后缀. 通过使用p.String计算映射键并将后缀附加到存储在该键下的切片,我们将新的前缀/后缀组合添加到chain映射中.

内置的append函数将元素追加到切片,并在必要时分配新的存储. 当提供的slice为nilappend分配一个新的slice. 此行为很方便地与我们地图的语义联系在一起:检索未设置的键将返回值类型的零值,而[]string的零值为nil . 当我们的程序遇到新的前缀(在映射中输入nil值)时, append将分配一个新的slice.

有关一般append函数和切片的更多信息,请参见" 切片:用法和内部知识"文章.
doc/codewalk/markov.go:96,97
将后缀压入前缀
在读取下一个单词之前,我们的算法要求我们从前缀中删除第一个单词,并将当前后缀压入前缀.

在这种状态下
  p ==前缀{" I"," am"}
 s =="不是" 
p的新值是
  p ==前缀{" am"," not"} 
在文本生成过程中也需要执行此操作,因此我们将代码用于执行切片的此突变,将其置于Prefix名为Shift的方法中.
doc/codewalk/markov.go:98
Shift方法
The Shift method uses the built-in copy function to copy the last len(p)-1 elements of p to the start of the slice, effectively moving the elements one index to the left (if you consider zero as the leftmost index).
p := Prefix{"I", "am"}
copy(p, p[1:])
// p == Prefix{"am", "am"}
We then assign the provided word to the last index of the slice:
// suffix == "not"
p[len(p)-1] = suffix
// p == Prefix{"am", "not"}
doc/codewalk/markov.go:68,71
产生文字
Generate方法与Build类似,不同之处在于, Generate方法不是从Reader中读取单词并将它们存储在地图中,而是从地图中读取单词并将它们附加到一个切片( words )上.

Generate使用条件for循环生成最多n字.
doc/codewalk/markov.go:103,116
获得潜在的后缀
在循环的每次迭代中,我们都会检索当前前缀的潜在后缀列表. 我们通过键p.String()访问chain映射,并将其内容分配给choices .

如果len(choices)为零,则因为该前缀没有潜在的后缀,所以我们跳出了循环. 如果键根本没有出现在地图中,则该测试也将起作用:在这种情况下, choices将为nil ,而nil slice的长度为零.
doc/codewalk/markov.go:107,110
随机选择后缀
要选择后缀,我们使用rand.Intn函数. 它返回一个直到(但不包括)所提供值的随机整数. 传递len(choices)给我们一个进入列表全长的随机索引.

我们使用该索引来选择新的后缀,将其分配给next并将其附加到words slice上.

接下来,我们Shift新后缀上,就像我们在做前缀Build方法.
doc/codewalk/markov.go:111,113
返回生成的文本
在将生成的文本作为字符串返回之前,我们使用strings.Join函数将words slice的元素连接在一起,并用空格分隔.
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我们使用prefix标志的值调用NewChain .

为了构建链,我们使用os.Stdin (实现io.Reader )来调用Build ,以便它将从标准输入中读取其输入.
doc/codewalk/markov.go:126,127
生成和打印文本
最后,要生成文本,我们使用words Generate的值调用Generate并将结果分配给变量text .

然后,我们调用fmt.Println将文本写入标准输出,然后回车.
doc/codewalk/markov.go:128,129
使用这个程序
要使用此程序,请首先使用go命令对其进行构建:
  $去建立markov.go 
然后在输入一些输入文本时执行它:
  $ echo"一个男人一个计划一个巴拿马运河"
	 |  ./markov -prefix = 1
 一个男人计划一个运河巴拿马 
以下是使用Go发行版的README文件作为源材料生成一些文本的记录:
  $ ./markov -words = 10 <$ GOROOT / README
 这是Go源码的源代码存储库
 $ ./markov -prefix = 1 -words = 10 <$ GOROOT / README
 这是go目录(包含此README的目录).
 $ ./markov -prefix = 1 -words = 10 <$ GOROOT / README
 如果您刚刚解压了,这就是变量 
doc/codewalk/markov.go
读者练习
Generate words切片时, Generate函数会进行大量分配. 作为一个练习,修改它采取一种io.Writer到它逐步与写入生成的文本Fprint . 除了提高效率之外,这还使" GenerateBuild更加对称.
doc/codewalk/markov.go

by  ICOPY.SITE