将文本预测脚本[马尔可夫链]从JavaScript转换为Python

5 投票
3 回答
1618 浏览
提问于 2025-04-17 15:30

我这几天一直在尝试把这个JavaScript脚本转换成Python代码。

到目前为止,我的实现(大部分是直接复制,做了一些小修正)是这样的:

import random
class markov:
    memory = {}
    separator = ' '
    order = 2

    def getInitial(self):
        ret = []
        for i in range(0, self.order, 1):
            ret.append('')
        return ret

    def breakText(self, txt, cb):
        parts = txt.split(self.separator)
        prev = self.getInitial()
        def step(self):
            cb(prev, self.next)
            prev.shift()#Javascript function.
            prev.append(self.next)
        #parts.forEach(step) # - step is the function above.
        cb(prev, '')

    def learn(self, txt):
        mem = self.memory
        def learnPart(key, value):
            if not mem[key]:
                mem[key] = []
            mem[key] = value
            return mem
        self.breakText(txt, learnPart)

    def step(self, state, ret):
        nextAvailable = self.memory[state] or ['']
        self.next = nextAvailable[random.choice(nextAvailable.keys())]
        if not self.next:
            return ret
        ret.append(next)
        nextState = state.slice(1)
        return self.step(nextState, ret)

    def ask(self, seed):
        if not seed:
            seed = self.genInitial()
        seed = seed + self.step(seed, []).join(self.separator)
        return seed

遇到的问题:

  1. 我对JavaScript完全不了解。

  2. 当我尝试把一些文本“学习”到一个“markov”类的对象里(比如:a=markov(); a.learn("sdfg");)时,出现了一个错误:“TypeError: unhashable type: 'list'”,这个错误出现在“learn”函数里的“learnPart”函数中的“mem”字典里。

    所以我现在的问题是,为什么会出现这个异常(对于一个列表对象的TypeError,错误地指向了一个字典对象(字典是可以哈希的))呢?

提前感谢任何建议、方向、意见和帮助 :D

3 个回答

1

我做了一个简化版的代码:

import re
class Brain():
    H = ''
    def learn(self, txt):
        self.H = txt
    def ask(self,ask):
        H=self.H
        ask = re.compile(r"%s(.*)"%(ask),re.I|re.DOTALL)
        m = ask.search(H)
        print m.group(1)

这是执行的结果:

>>> import brain
>>> bob = brain.Brain()
>>> bob.learn('Mary had a little lamb' )
>>> bob.ask('Mary had')
'a little lamb'

我同意这并不完全是一个马尔可夫链算法。但它有几个优点:

i. 你可以像上面那样直接给ask()输入原始文本。

ii. 代码行数更少。

iii. 希望它更容易理解。

1

复杂的答案

这里的问题是,learnPart 试图把 getInitial 返回的一个列表当作字典的键来用。列表是可变的,也就是说它们的内容可以改变,因此不能被用作字典的键。

你可以试着在 learnPart 中添加这一行:

def learnPart(key, value):
    key = tuple(key) #<-----Try adding this line
    if not mem[key]:
        mem[key] = []
    mem[key] = value
    return mem

但我觉得这并不能解决所有的问题。

简单的答案

网上有很多用 Python 写的马尔可夫链实现。快速在 Github 上搜索一下,发现有 168 个相关项目:https://github.com/search?l=Python&q=markov+chain

15

这篇文章的作者在这里。很高兴你觉得它有用!我最初实现马尔可夫链的代码其实是用Python写的,所以接下来的内容会专注于如何用更符合Python风格的方式来写。我会展示如何制作一个二阶的马尔可夫链,因为这个比较容易理解,不过你当然可以通过一些修改来做成N阶的。

数据结构

在JavaScript中,主要的数据结构是通用对象和数组(数组是通用对象的扩展)。而在Python中,你有更多的选择,可以更细致地控制数据结构。下面是这两种实现的主要区别:

  • 我们链中的一个状态实际上是一个元组——它是一个不可变的、有序的结构,包含固定数量的元素。我们总是需要 n 个元素(在这个例子中,n=2),而且它们的顺序是有意义的。

  • 如果我们使用一个 defaultdict 来包装一个列表,操作内存会更简单,这样我们就可以省去“检查一个状态是否存在,然后再做X”的步骤,直接做X就可以了。

所以,我们在顶部加上 from collections import defaultdict,并修改 markov.memory 的定义:

memory = defaultdict(list)

现在我们把 markov.getInitial 改成返回一个元组(记住这解释的是一个二阶链):

def getInitial(self):
    return ('', '')

(如果你想进一步扩展,可以使用一个很酷的Python技巧:tuple([''] * 2) 会返回相同的东西。你可以用 None 替代空字符串)

稍后我们会修改使用 genInitial 的部分。

生成和迭代

在JavaScript中没有(但在Python中有)的一个强大概念是 yield 操作符(查看这个问题可以获得很好的解释)。

Python的另一个特点是它的通用 for 循环。你可以很容易地遍历几乎任何东西,包括生成器(使用 yield 的函数)。结合这两者,我们可以重新定义 breakText

def breakText(self, txt):
    #our very own (ε,ε)
    prev = self.getInitial()

    for word in txt.split(self.separator):
        yield prev, word
        #will be explained in the next paragraph
        prev = (prev[1], word)

    #end-of-sentence, prev->ε
    yield prev, ''

上面那部分神奇的代码 prev = (prev[1], word) 用例子来解释会更清楚:

>>> a = (0, 1)
>>> a
(0, 1)
>>> a = (a[1], 2)
>>> a
(1, 2)

这就是我们如何在单词列表中前进的。现在我们来看看使用 breakText 的部分,重新定义 markov.learn

def learn(self, txt):
    for part in self.breakText(txt):
        key = part[0]
        value = part[1]

        self.memory[key].append(value)

因为我们的内存是一个 defaultdict,所以我们不需要担心键不存在的问题。

路边小憩

好了,我们已经实现了链的一半,接下来看看它的实际效果!到目前为止我们有:

from collections import defaultdict

class Markov:
    memory = defaultdict(list)
    separator = ' '

    def learn(self, txt):
        for part in self.breakText(txt):
            key = part[0]
            value = part[1]

            self.memory[key].append(value)

    def breakText(self, txt):
        #our very own (ε,ε)
        prev = self.getInitial()

        for word in txt.split(self.separator):
            yield prev, word
            prev = (prev[1], word)

        #end-of-sentence, prev->ε
        yield (prev, '')

    def getInitial(self):
        return ('', '')

(我把类名从 markov 改成了 Markov,因为每次看到类名以小写字母开头我都觉得不舒服)。我把它保存为 brain.py 并启动了Python。

>>> import brain
>>> bob = brain.Markov()
>>> bob.learn('Mary had a little lamb')
>>> bob.memory
defaultdict(<class 'list'>, {('had', 'a'): ['little'], ('Mary', 'had'): ['a'], ('', ''): ['Mary'], ('little', 'lamb'): [''], ('a', 'little'): ['lamb'], ('', 'Mary'): ['had']})

成功了!让我们仔细看看结果,确认我们做对了:

{ ('', ''): ['Mary'],
  ('', 'Mary'): ['had'],
  ('Mary', 'had'): ['a'],
  ('a', 'little'): ['lamb'],
  ('had', 'a'): ['little'],
  ('little', 'lamb'): ['']}

拉上拉链 准备继续开车了吗?我们还得 使用 这个链!

修改 step 函数

我们已经了解了重做 step 所需的内容。我们有了defaultdict,所以可以直接使用 random.choice,而且我可以稍微作弊一下,因为我知道链的顺序。如果我们把它看作是通过链走一步的函数,我们也可以去掉递归(这让我有点遗憾),这在原文中是个命名不当的函数)。

def step(self, state):
    choice = random.choice(self.memory[state] or [''])

    if not choice:
        return None

    nextState = (state[1], choice)
    return choice, nextState

我遗憾地加上了 or [''],因为 random.choice 对空列表会发愁。最后,我们把更多的逻辑移到 ask(句子的实际构建)中:

def ask(self, seed=False):
    ret = []

    if not seed:
        seed = self.getInitial()

    while True:
        link = self.step(seed)

        if link is None:
            break

        ret.append(link[0])
        seed = link[1]

    return self.separator.join(ret)

是的,有点麻烦。我们本可以给 step 起个更好的名字,并把它做成生成器,但我现在要赶去见我怀孕的妻子,她快要生了,车里还有个小孩把炉子开着,车子正在被拖走!我得快点!

大结局

但首先,和bob聊聊:

from collections import defaultdict
import random

class Markov:
    memory = defaultdict(list)
    separator = ' '

    def learn(self, txt):
        for part in self.breakText(txt):
            key = part[0]
            value = part[1]

            self.memory[key].append(value)

    def ask(self, seed=False):
        ret = []

        if not seed:
            seed = self.getInitial()

        while True:
            link = self.step(seed)

            if link is None:
                break

            ret.append(link[0])
            seed = link[1]

        return self.separator.join(ret)

    def breakText(self, txt):
        #our very own (ε,ε)
        prev = self.getInitial()

        for word in txt.split(self.separator):
            yield prev, word
            prev = (prev[1], word)

        #end-of-sentence, prev->ε
        yield (prev, '')

    def step(self, state):
        choice = random.choice(self.memory[state] or [''])

        if not choice:
            return None

        nextState = (state[1], choice)
        return choice, nextState

    def getInitial(self):
        return ('', '')

然后加载它:

>>> import brain
>>> bob = brain.Markov()
>>> bob.learn('Mary had a little lamb')
>>> bob.ask()
'Mary had a little lamb'
>>> bob.learn('Mary had a giant crab')
>>> bob.ask(('Mary', 'had'))
'a giant crab'

当然,这个概念还有改进和扩展的空间。但如果我直接给你答案,那就没意思了。

希望这在四个月后仍然能帮到你。

撰写回答