0%

一场关于大模型的战役正在全世界激烈地上演着,国内外的各大科技巨头和研究机构纷纷投入到这场战役中,光是写名字就能罗列出一大串,比如国外的有 OpenAI 的 GPT-4,Meta 的 LLaMa,Stanford University 的 Alpaca,Google 的 LaMDAPaLM 2,Anthropic 的 Claude,Databricks 的 Dolly,国内的有百度的 文心,阿里的 通义,科大讯飞的 星火,华为的 盘古,复旦大学的 MOSS,智谱 AI 的 ChatGLM 等等等等。

一时间大模型如百花齐放,百鸟争鸣,并在向各个行业领域渗透,让人感觉通用人工智能仿佛就在眼前。基于大模型开发的应用和产品也如雨后春笋,让人目不暇接,每天都有很多新奇的应用和产品问世,有的可以充当你的朋友配你聊天解闷,有的可以充当你的老师帮你学习答疑,有的可以帮你写文章编故事,有的可以帮你写代码改 BUG,大模型的崛起正影响着我们生活中的方方面面。

正是在这样的背景下,为了方便和统一基于大模型的应用开发,一批大模型应用开发框架横空出世,LangChain 就是其中最流行的一个。

快速开始

正如前文所述,LangChain 是一个基于大语言模型(LLM)的应用程序开发框架,它提供了一整套工具、组件和接口,简化了创建大模型应用程序的过程,方便开发者使用语言模型实现各种复杂的任务,比如聊天机器人、文档问答、各种基于 Prompt 的助手等。根据 官网的介绍,它可以让你的应用变得 Data-awareAgentic

  • Data-aware:也就是数据感知,可以将语言模型和其他来源的数据进行连接,比如让语言模型针对指定文档回答问题;
  • Agentic:可以让语言模型和它所处的环境进行交互,实现类似代理机器人的功能,帮助用户完成指定任务;

LangChain 在 GitHub 上有着异乎寻常的热度,截止目前为止,星星数高达 55k,而且它的更新非常频繁,隔几天就会发一个新版本,有时甚至一天发好几个版本,所以学习的时候最好以官方文档为准,网络上有很多资料都过时了(包括我的这篇笔记)。

LangChain 提供了 PythonJavaScript 两个版本的 SDK,这里我主要使用 Python 版本的,在我写这篇笔记的时候,最新的版本为 0.0.238,使用下面的命令安装:

1
$ pip install langchain==0.0.238

注意:Python 版本需要在 3.8.1 及以上,如果低于这个版本,只能安装 langchain==0.0.27

另外要注意的是,这个命令只会安装 LangChain 的基础包,这或许并没有什么用,因为 LangChain 最有价值的地方在于它能和各种各样的语言模型、数据存储、外部工具等进行交互,比如如果我们需要使用 OpenAI,则需要手动安装:

1
$ pip install openai

也可以在安装 LangChain 时指定安装可选依赖包:

1
$ pip install langchain[openai]==0.0.238

或者使用下面的命令一次性安装所有的可选依赖包(不过很多依赖可能会用不上):

1
$ pip install langchain[all]==0.0.238

LangChain 支持的可选依赖包有:

1
2
3
4
5
6
7
8
9
10
11
llms = ["anthropic", "clarifai", "cohere", "openai", "openllm", "openlm", "nlpcloud", "huggingface_hub", ... ]
qdrant = ["qdrant-client"]
openai = ["openai", "tiktoken"]
text_helpers = ["chardet"]
clarifai = ["clarifai"]
cohere = ["cohere"]
docarray = ["docarray"]
embeddings = ["sentence-transformers"]
javascript = ["esprima"]
azure = [ ... ]
all = [ ... ]

可以在项目的 pyproject.toml 文件中查看依赖包详情。

入门示例:LLMs vs. ChatModels

我们首先从一个简单的例子开始:

1
2
3
4
5
6
7
from langchain.llms import OpenAI

llm = OpenAI(temperature=0.9)
response = llm.predict("给水果店取一个名字")
print(response)

# 果舞时光

LangChain 集成了许多流行的语言模型,并提供了一套统一的接口方便开发者直接使用,比如在上面的例子中,我们引入了 OpenAI 这个 LLM,然后调用 llm.predict() 方法让语言模型完成后续内容的生成。如果用户想使用其他语言模型,只需要将上面的 OpenAI 换成其他的即可,比如流行的 Anthropic 的 Claude 2,或者 Google 的 PaLM 2 等,这里 可以找到 LangChain 目前支持的所有语言模型接口。

回到上面的例子,llm.predict() 方法实际上调用的是 OpenAI 的 Completions 接口,这个接口的作用是给定一个提示语,让 AI 生成后续内容;我们知道,除了 Completions,OpenAI 还提供了一个 Chat 接口,也可以用于生成后续内容,而且比 Completions 更强大,可以给定一系列对话内容,让 AI 生成后续的回复,从而实现类似 ChatGPT 的聊天功能。

官方推荐使用 Chat 替换 Completions 接口,在后续的 OpenAI 版本中,Completions 接口可能会被弃用。

因此,LangChain 也提供 Chat 接口:

1
2
3
4
5
6
7
8
9
10
from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage

chat = ChatOpenAI(temperature=0.9)
response = chat.predict_messages([
HumanMessage(content="窗前明月光,下一句是什么?"),
])
print(response.content)

# 疑是地上霜。

和上面的 llm.predict() 方法比起来,chat.predict_messages() 方法可以接受一个数组,这也意味着 Chat 接口可以带上下文信息,实现聊天的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain.chat_models import ChatOpenAI
from langchain.schema import AIMessage, HumanMessage, SystemMessage

chat = ChatOpenAI(temperature=0.9)
response = chat.predict_messages([
SystemMessage(content="你是一个诗词助手,帮助用户回答诗词方面的问题"),
HumanMessage(content="窗前明月光,下一句是什么?"),
AIMessage(content="疑是地上霜。"),
HumanMessage(content="这是谁的诗?"),
])
print(response.content)

# 这是李白的《静夜思》。

另外,Chat 接口也提供了一个 chat.predict() 方法,可以实现和 llm.predict() 一样的效果:

1
2
3
4
5
6
7
from langchain.chat_models import ChatOpenAI

chat = ChatOpenAI(temperature=0.9)
response = chat.predict("给水果店取一个名字")
print(response)

# 果香居

实现翻译助手:PromptTemplate

week040-chrome-extension-with-chatgpt 这篇笔记中,我们通过提示语技术实现了一个非常简单的划词翻译 Chrome 插件,其中的翻译功能我们也可以使用 LangChain 来完成,当然,使用 LLMsChatModels 都可以。

使用 LLMs 实现翻译助手:

1
2
3
4
5
6
7
from langchain.llms import OpenAI

llm = OpenAI(temperature=0.9)
response = llm.predict("将下面的句子翻译成英文:今天的天气真不错")
print(response)

# The weather is really nice today.

使用 ChatModels 实现翻译助手:

1
2
3
4
5
6
7
8
9
10
11
from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage

chat = ChatOpenAI(temperature=0.9)
response = chat.predict_messages([
SystemMessage(content="你是一个翻译助手,可以将中文翻译成英文。"),
HumanMessage(content="今天的天气真不错"),
])
print(response.content)

# The weather is really nice today.

观察上面的代码可以发现,输入参数都具备一个固定的模式,为此,LangChain 提供了一个 PromptTemplate 类来方便我们构造提示语模板:

1
2
3
4
5
6
7
8
9
10
11
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate

prompt = PromptTemplate.from_template("将下面的句子翻译成英文:{sentence}")
text = prompt.format(sentence="今天的天气真不错")

llm = OpenAI(temperature=0.9)
response = llm.predict(text)
print(response)

# Today's weather is really great.

其实 PromptTemplate 默认实现就是 Python 的 f-strings,只不过它提供了一种抽象,还可以支持其他的模板实现,比如 jinja2 模板引擎

对于 ChatModels,LangChain 也提供了相应的 ChatPromptTemplate,只不过使用起来要稍微繁琐一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate

system_message_prompt = SystemMessagePromptTemplate.from_template(
"你是一个翻译助手,可以将{input_language}翻译成{output_language}。")
human_message_prompt = HumanMessagePromptTemplate.from_template("{text}")
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])
messages = chat_prompt.format_messages(input_language="中文", output_language="英文", text="今天的天气真不错")

chat = ChatOpenAI(temperature=0.9)
response = chat.predict_messages(messages)
print(response.content)

# The weather today is really good.

实现知识库助手:Data connection

week042-doc-qa-using-embedding 这篇笔记中,我们通过 OpenAI 的 Embedding 接口和开源向量数据库 Qdrant 实现了一个非常简单的知识库助手。在上面的介绍中我们提到,LangChain 的一大特点就是数据感知,可以将语言模型和其他来源的数据进行连接,所以知识库助手正是 LangChain 最常见的用例之一,这一节我们就使用 LangChain 来重新实现它。

LangChain 将实现知识库助手的过程拆分成了几个模块,可以自由组合使用,这几个模块是:

  • Document loaders - 用于从不同的来源加载文档;
  • Document transformers - 对文档进行处理,比如转换为不同的格式,对大文档分片,去除冗余文档,等等;
  • Text embedding models - 通过 Embedding 模型将文本转换为向量;
  • Vector stores - 将文档保存到向量数据库,或从向量数据库中检索文档;
  • Retrievers - 用于检索文档,这是比向量数据库更高一级的抽象,不仅仅限于从向量数据库中检索,可以扩充更多的检索来源;

读取文档

TextLoader 是最简单的读取文档的方法,它可以处理大多数的纯文本,比如 txtmd 文件:

1
2
3
4
5
from langchain.document_loaders import TextLoader

loader = TextLoader("./kb.txt")
raw_documents = loader.load()
print(raw_documents)

LangChain 还提供了一些其他格式的文档读取方法,比如 JSONHTMLCSVWordPPTPDF 等,也可以加载其他来源的文档,比如通过 URLLoader 抓取网页内容,通过 WikipediaLoader 获取维基百科的内容等,还可以使用 DirectoryLoader 同时读取整个目录的文档。

文档分割

使用 TextLoader 加载得到的是原始文档,有时候我们还需要对原始文档进行处理,最常见的一种处理方式是文档分割。由于大模型的输入存在上下文窗口的限制,所以我们不能直接将一个几百兆的文档丢给大模型,而是将大文档分割成一个个小分片,然后通过 Embedding 技术查询与用户问题最相关的几个分片丢给大模型。

最简单的文档分割器是 CharacterTextSplitter,它默认使用分隔符 \n\n 来分割文档,我们也可以修改为使用 \n 来分割:

1
2
3
4
5
6
7
8
9
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator = "\n",
chunk_size = 0,
chunk_overlap = 0,
length_function = len,
)
documents = text_splitter.split_documents(raw_documents)
print(documents)

CharacterTextSplitter 可以通过 chunk_size 参数控制每个分片的大小,默认情况下分片大小为 4000,这也意味着如果有长度不足 4000 的分片会递归地和其他分片合并成一个分片;由于我这里的测试文档比较小,总共都没有 4000,所以按 \n 分割完,每个分片又会被合并成一个大分片了,所以我这里将 chunk_size 设置为 0,这样就不会自动合并。

另一个需要注意的是,这里的分片大小是根据 length_function 来计算的,默认使用的是 len() 函数,也就是根据字符的长度来分割;但是大模型的上下文窗口限制一般是通过 token 来计算的,这和长度有一些细微的区别,所以如果要确保分割后的分片能准确的适配大模型,我们就需要 通过 token 来分割文档,比如 OpenAI 的 tiktoken,Hugging Face 的 GPT2TokenizerFast 等。OpenAI 的这篇文档 介绍了什么是 token 以及如何计算它,感兴趣的同学可以参考之。

不过在一般情况下,如果对上下文窗口的控制不需要那么严格,按长度分割也就足够了。

Embedding 与向量库

有了分割后的文档分片,接下来,我们就可以对每个分片计算 Embedding 了:

1
2
3
4
5
from langchain.embeddings.openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()
doc_result = embeddings.embed_documents(['你好', '再见'])
print(doc_result)

这里我们使用的是 OpenAI 的 Embedding 接口,除此之外,LangChain 还集成了很多 其他的 Embedding 接口,比如 Cohere、SentenceTransformer 等。不过一般情况下我们不会像上面这样单独计算 Embedding,而是和向量数据库结合使用:

1
2
3
4
5
6
7
8
9
10
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Qdrant

qdrant = Qdrant.from_documents(
documents,
OpenAIEmbeddings(),
url="127.0.0.1:6333",
prefer_grpc=False,
collection_name="my_documents",
)

这里我们仍然使用 Qdrant 来做向量数据库,Qdrant.from_documents() 方法会自动根据文档列表计算 Embedding 并存储到 Qdrant 中。除了 Qdrant,LangChain 也集成了很多 其他的向量数据库,比如 Chroma、FAISS、Milvus、PGVector 等。

再接下来,我们通过 qdrant.similarity_search() 方法从向量数据库中搜索出和用户问题最接近的文本片段:

1
2
3
query = "小明家的宠物狗叫什么名字?"
found_docs = qdrant.similarity_search(query)
print(found_docs)

后面的步骤就和 week042-doc-qa-using-embedding 这篇笔记中一样,准备一段提示词模板,向 ChatGPT 提问就可以了。

LangChain 的精髓:Chain

通过上面的快速开始,我们学习了 LangChain 的基本用法,从几个例子下来,或许有人觉得 LangChain 也没什么特别的,只是一个集成了大量的 LLM、Embedding、向量库的 SDK 而已,我一开始也是这样的感觉,直到学习 Chain 这个概念的时候,才明白这时才算是真正地进入 LangChain 的大门。

LLMChain 开始

LLMChain 是 LangChain 中最基础的 Chain,它接受两个参数:一个是 LLM,另一个是提供给 LLM 的 Prompt 模板:

1
2
3
4
5
6
7
8
9
10
11
from langchain import PromptTemplate, OpenAI, LLMChain

llm = OpenAI(temperature=0.9)
prompt = PromptTemplate.from_template("将下面的句子翻译成英文:{sentence}")

llm_chain = LLMChain(
llm = llm,
prompt = prompt
)
result = llm_chain("今天的天气真不错")
print(result['text'])

其中,LLM 参数同时支持 LLMsChatModels,运行效果和上面的入门示例是一样的。

在 LangChain 中,Chain 的特别之处在于,它的每一个参数都被称为 Component,Chain 由一系列的 Component 组合在一起以完成特定任务,Component 也可以是另一个 Chain,通过封装和组合,形成一个更复杂的调用链,从而创建出更强大的应用程序。

上面的例子中,有一点值得注意的是,我们在 Prompt 中定义了一个占位符 {sentence},但是在调用 Chain 的时候并没有明确指定该占位符的值,LangChain 是怎么知道要将我们的输入替换掉这个占位符的呢?实际上,每个 Chain 里都包含了两个很重要的属性:input_keysoutput_keys,用于表示这个 Chain 的输入和输出,我们查看 LLMChain 的源码,它的 input_keys 实现如下:

1
2
3
@property
def input_keys(self) -> List[str]:
return self.prompt.input_variables

所以 LLMChain 的入参就是 Prompt 的 input_variables,而 PromptTemplate.from_template() 其实是 PromptTemplate 的简写,下面这行代码:

1
prompt = PromptTemplate.from_template("将下面的句子翻译成英文:{sentence}")

等价于下面这行代码:

1
2
3
4
prompt = PromptTemplate(
input_variables=['sentence'],
template = "将下面的句子翻译成英文:{sentence}"
)

所以 LLMChain 的入参也就是 sentence 参数。

而下面这行代码:

1
result = llm_chain("今天的天气真不错")

是下面这行代码的简写:

1
result = llm_chain({'sentence': "今天的天气真不错"})

当参数比较简单时,LangChain 会自动将传入的参数转换成它需要的 Dict 形式,一旦我们在 Prompt 中定义了多个参数,那么这种简写就不行了,就得在调用的时候明确指定参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain import PromptTemplate, OpenAI, LLMChain

llm = OpenAI(temperature=0.9)
prompt = PromptTemplate(
input_variables=['lang', 'sentence'],
template = "将下面的句子翻译成{lang}:{sentence}"
)

llm_chain = LLMChain(
llm = llm,
prompt = prompt
)
result = llm_chain({"lang": "日语", "sentence": "今天的天气真不错"})
print(result['text'])

# 今日の天気は本当に良いです。

我们从 LangChain 的源码中可以更深入的看下 Chain 的类定义,如下:

1
2
3
4
5
6
7
8
9
10
class Chain(BaseModel, ABC):

def __call__(
self,
inputs: Union[Dict[str, Any], Any],
return_only_outputs: bool = False,
callbacks: Callbacks = None,
...
) -> Dict[str, Any]:
...

这说明 Chain 的本质其实就是根据一个 Dict 输入,得到一个 Dict 输出而已。只不过 Chain 为我们还提供了三个特性:

  • Stateful: 内置 Memory 记忆功能,使得每个 Chain 都是有状态的;
  • Composable: 具备高度的可组合性,我们可以将 Chain 和其他的组件,或者其他的 Chain 进行组合;
  • Observable: 支持向 Chain 传入 Callbacks 回调执行额外功能,从而实现可观测性,如日志、监控等;

使用 Memory 实现记忆功能

Chain 的第一个特征就是有状态,也就是说,它能够记住会话的历史,这是通过 Memory 模块来实现的,最简单的 Memory 是 ConversationBufferMemory,它通过一个内存变量保存会话记忆。

在使用 Memory 时,我们的 Prompt 需要做一些调整,我们要在 Prompt 中加上历史会话的内容,比如下面这样:

1
2
3
4
5
6
7
8
template = """
下面是一段人类和人工智能之间的友好对话。

当前对话内容:
{history}
Human: {input}
AI:"""
prompt = PromptTemplate(input_variables=["history", "input"], template=template)

其中 {history} 这个占位符的内容就由 Memory 来负责填充,我们只需要将 Memory 设置到 LLMChain 中,这个填充操作将由框架自动完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
llm = OpenAI(temperature=0.9)
memory = ConversationBufferMemory()

llm_chain = LLMChain(
llm = llm,
prompt = prompt,
memory = memory
)

result = llm_chain("窗前明月光,下一句是什么?")
print(result['text'])

result = llm_chain("这是谁的诗?")
print(result['text'])

对于 ChatModels,我们可以使用 ChatPromptTemplate 来构造 Prompt:

1
2
3
4
5
6
7
prompt = ChatPromptTemplate(
messages=[
SystemMessagePromptTemplate.from_template("你是一个聊天助手,和人类进行聊天。"),
MessagesPlaceholder(variable_name="history"),
HumanMessagePromptTemplate.from_template("{input}")
]
)

占位符填充的基本逻辑是一样的,不过在初始化 Memory 的时候记得指定 return_messages=True,让 Memory 使用 Message 列表来填充占位符,而不是默认的字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chat = ChatOpenAI(temperature=0.9)
memory = ConversationBufferMemory(return_messages=True)

llm_chain = LLMChain(
llm = chat,
prompt = prompt,
memory = memory
)

result = llm_chain("窗前明月光,下一句是什么?")
print(result['text'])

result = llm_chain("这是谁的诗?")
print(result['text'])

为了简化 Prompt 的写法,LangChain 基于 LLMChain 实现了 ConversationChain,使用起来更简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory

chat = ChatOpenAI(temperature=0.9)
memory = ConversationBufferMemory()

conversation = ConversationChain(
llm = chat,
memory = memory
)

result = conversation.run("窗前明月光,下一句是什么?")
print(result)

result = conversation.run("这是谁的诗?")
print(result)

除了 ConversationBufferMemory,LangChain 还提供了很多其他的 Memory 实现:

  • ConversationBufferWindowMemory - 基于内存的记忆,只保留最近 K 次会话;
  • ConversationTokenBufferMemory - 基于内存的记忆,根据 token 数来限制保留最近的会话;
  • ConversationEntityMemory - 从会话中提取实体,并在记忆中构建关于实体的知识;
  • ConversationKGMemory - 基于知识图来重建记忆;
  • ConversationSummaryMemory - 对历史会话进行总结,减小记忆大小;
  • ConversationSummaryBufferMemory - 既保留最近 K 次会话,也对历史会话进行总结;
  • VectorStoreRetrieverMemory - 通过向量数据库保存记忆;

这些 Memory 在长对话场景,或需要对记忆进行持久化时非常有用。内置的 Memory 大多数都是基于内存实现的,LangChain 还集成了很多第三方的库,可以实现记忆的持久化,比如 Sqlite、Mongo、Postgre 等。

再聊文档问答:RetrievalQA

在上面的实现知识库助手一节,我们学习了各种加载文档的方式,以及如何对大文档进行分片,并使用 OpenAI 计算文档的 Embedding,最后保存到向量数据库 Qdrant 中。一旦文档库准备就绪,接下来就可以对它进行问答了,LangChain 也贴心地提供了很多关于文档问答的 Chain 方便我们使用,比如下面最常用的 RetrievalQA

1
2
3
4
5
6
7
8
9
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI

qa = RetrievalQA.from_chain_type(llm=OpenAI(), chain_type="stuff", retriever=qdrant.as_retriever())
query = "小明家的宠物狗比小红家的大几岁?"
result = qa.run(query)
print(result)

# 毛毛比大白大两岁,毛毛今年3岁,大白今年1岁。

RetrievalQA 充分体现了 Chain 的可组合性,它实际上是一个复合 Chain,真正实现文档问答的 Chain 是它内部封装的 CombineDocumentsChainRetrievalQA 的作用是通过 Retrievers 获取和用户问题相关的文档,然后丢给内部的 CombineDocumentsChain 来处理,而 CombineDocumentsChain 又是一个复合 Chain,它会将用户问题和相关文档组合成提示语,丢到内部的 LLMChain 处理最终得到输出。

但总的来说,实现文档问答最核心的还是 RetrievalQA 内部使用的 CombineDocumentsChain,实际上除了 文档问答,针对文档的使用场景还有 生成文档摘要提取文档信息,等等,这些利用大模型处理文档的场景中,最难解决的问题是如何对大文档进行拆分和组合,而解决方案也是多种多样的,LangChain 提供的 CombineDocumentsChain 是一个抽象类,根据不同的解决方案它包含了四个实现类:

StuffDocumentsChain

StuffDocumentsChain 是最简单的文档处理 Chain,单词 stuff 的意思是 填充,正如它的名字所示,StuffDocumentsChain 将所有的文档内容连同用户的问题一起全部填充进 Prompt,然后丢给大模型。这种 Chain 在处理比较小的文档时非常有用。在上面的例子中,我们通过 RetrievalQA.from_chain_type(chain_type="stuff") 生成了 RetrievalQA,内部就是使用了 StuffDocumentsChain。它的示意图如下所示:

RefineDocumentsChain

refine 的含义是 提炼RefineDocumentsChain 表示从分片的文档中一步一步的提炼出用户问题的答案,这个 Chain 在处理大文档时非常有用,可以保证每一步的提炼都不超出大模型的上下文限制。比如我们有一个很大的 PDF 文件,我们要生成它的文档摘要,我们可以先生成第一个分片的摘要,然后将第一个分片的摘要和第二个分片丢给大模型生成前两个分片的摘要,再将前两个分片的摘要和第三个分片丢给大模型生成前三个分片的摘要,依次类推,通过不断地对大模型的答案进行更新,最终生成全部分片的摘要。整个提炼的过程如下:

RefineDocumentsChain 相比于 StuffDocumentsChain,会产生更多的大模型调用,而且对于有些场景,它的效果可能很差,比如当文档中经常出现相互交叉的引用时,或者需要从多个文档中获取非常详细的信息时。

MapReduceDocumentsChain

MapReduceDocumentsChainRefineDocumentsChain 一样,都适合处理一些大文档,它的处理过程可以分为两步:MapReduce。比如我们有一个很大的 PDF 文件,我们要生成它的文档摘要,我们可以先将文档分成小的分片,让大模型挨个生成每个分片的摘要,这一步称为 Map;然后再将每个分片的摘要合起来生成汇总的摘要,这一步称为 Reduce;如果所有分片的摘要合起来超过了大模型的限制,那么我们需要对合起来的摘要再次分片,然后再次递归地执行 MapReduce 这个过程:

MapRerankDocumentsChain

MapRerankDocumentsChain 一般适合于大文档的问答场景,它的第一步和 MapReduceDocumentsChain 类似,都是遍历所有的文档分片,挨个向大模型提问,只不过它的 Prompt 有一点特殊,它不仅要求大模型针对用户的问题给出一个答案,而且还要求大模型为其答案的确定性给出一个分数;得到每个答案的分数后,MapRerankDocumentsChain 的第二步就可以按分数排序,给出分数最高的答案:

对 Chain 进行调试

当 LangChain 的输出结果有问题时,开发者就需要 对 Chain 进行调试,特别是当组合的 Chain 非常多时,LangChain 的输出结果往往非常不可控。最简单的方法是在 Chain 上加上 verbose=True 参数,这时 LangChain 会将执行的中间步骤打印出来,比如上面的 使用 Memory 实现记忆功能 一节中的例子,加上调试信息后,第一次输出结果如下:

1
2
3
4
5
6
7
> Entering new LLMChain chain...
Prompt after formatting:
System: 你是一个聊天助手,和人类进行聊天。
Human: 窗前明月光,下一句是什么?

> Finished chain.
疑是地上霜。

第二次的输出结果如下:

1
2
3
4
5
6
7
8
9
> Entering new LLMChain chain...
Prompt after formatting:
System: 你是一个聊天助手,和人类进行聊天。
Human: 窗前明月光,下一句是什么?
AI: 疑是地上霜。
Human: 这是谁的诗?

> Finished chain.
这是《静夜思》的开头,是中国唐代诗人李白创作的。

可以很方便的看出 Memory 模块确实起作用了,它将历史会话自动拼接在 Prompt 中了。

当一个 Chain 组合了其他 Chain 的时候,比如上面的 RetrievalQA,这时我们不仅要给 Chain 加上 verbose=True 参数,注意还要通过 chain_type_kwargs 为内部的 Chain 也加上该参数:

1
2
3
4
5
6
qa = RetrievalQA.from_chain_type(
llm=OpenAI(),
chain_type="stuff",
retriever=qdrant.as_retriever(),
verbose=True,
chain_type_kwargs={'verbose': True})

输出来的结果类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> Entering new RetrievalQA chain...

> Entering new StuffDocumentsChain chain...

> Entering new LLMChain chain...
Prompt after formatting:
Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.

小明家有一条宠物狗,叫毛毛,这是他爸从北京带回来的,它今年3岁了。

小红家也有一条宠物狗,叫大白,非常听话,它今年才1岁呢。

小红的好朋友叫小明,他们是同班同学。

小华是小明的幼儿园同学,从小就欺负他。

Question: 小明家的宠物狗比小红家的大几岁?
Helpful Answer:

> Finished chain.

> Finished chain.

> Finished chain.
毛毛比大白大2岁。

从输出的结果我们不仅可以看到各个 Chain 的调用链路,而且还可以看到 StuffDocumentsChain 所使用的提示语,以及 retriever 检索出来的内容,这对我们调试 Chain 非常有帮助。

另外,Callbacks 是 LangChain 提供的另一种调试机制,它提供了比 verbose=True 更好的扩展性,用户可以基于这个机制实现自定义的监控或日志功能。LangChain 内置了很多的 Callbacks,这些 Callbacks 都实现自 CallbackHandler 接口,比如最常用的 StdOutCallbackHandler,它将日志打印到控制台,参数 verbose=True 就是通过它实现的;或者 FileCallbackHandler 可以将日志记录到文件中;LangChain 还 集成了很多第三方工具,比如 StreamlitCallbackHandler 可以将日志以交互形式输出到 Streamlit 应用中。

用户如果要实现自己的 Callbacks,可以直接继承基类 BaseCallbackHandler 即可,下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MyCustomHandler(BaseCallbackHandler):

def on_llm_start(
self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
) -> Any:
"""Run when LLM starts running."""
print(f"on_llm_start: {serialized} {prompts}")

def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
"""Run when LLM ends running."""
print(f"on_llm_end: {response}")

def on_llm_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when LLM errors."""
print(f"on_llm_error: {error}")

def on_chain_start(
self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
) -> Any:
"""Run when chain starts running."""
print(f"on_chain_start: {serialized} {inputs}")

def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> Any:
"""Run when chain ends running."""
print(f"on_chain_end: {outputs}")

def on_chain_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when chain errors."""
print(f"on_chain_error: {error}")

def on_text(self, text: str, **kwargs: Any) -> Any:
"""Run on arbitrary text."""
print(f"on_text: {text}")

在这个例子中,我们对 LLM 和 Chain 进行了监控,我们需要将这个 Callback 分别传递到 LLM 和 Chain 中:

1
2
3
4
5
6
7
8
9
10
11
12
callback = MyCustomHandler()

llm = OpenAI(temperature=0.9, callbacks=[callback])
prompt = PromptTemplate.from_template("将下面的句子翻译成英文:{sentence}")

llm_chain = LLMChain(
llm = llm,
prompt = prompt,
callbacks=[callback]
)
result = llm_chain("今天的天气真不错")
print(result['text'])

LangChain 会在调用 LLM 和 Chain 开始和结束的时候回调我们的 Callback,输出结果大致如下:

1
2
3
4
5
6
on_chain_start: <...略>
on_text: Prompt after formatting:
将下面的句子翻译成英文:今天的天气真不错
on_llm_start: <...略>
on_llm_end: <...略>
on_chain_end: {'text': '\n\nThe weather today is really nice.'}

还有一种更简单的调试方法,直接将 langchain.debug 设置为 True 即可,如下:

1
2
3
import langchain

langchain.debug = True

从 Chain 到 Agent

通过上一节的学习,我们掌握了 Chain 的基本概念和用法,对 Chain 的三大特性也有了一个大概的认识,关于 Chain 还有很多高级主题等着我们去探索,比如 Chain 的异步调用实现自定义的 ChainChain 的序列化和反序列化从 LangChainHub 加载 Chain,等等。

此外,除了上一节学习的 LLMChainConversationChainRetrievalQA 以及各种 CombineDocumentsChain,LangChain 还提供了很多其他的 Chain 供用户使用,其中有四个是最基础的:

  • LLMChain - 这个 Chain 在前面已经学习过,它主要是围绕着大模型添加一些额外的功能,比如 ConversationChainCombineDocumentsChain 都是基于 LLMChain 实现的;
  • TransformChain - 这个 Chain 主要用于参数转换,这在对多个 Chain 进行组合时会很有用,我们可以使用 TransformChain 将上一个 Chain 的输出参数转换为下一个 Chain 的输入参数;
  • SequentialChain - 可以将多个 Chain 组合起来并按照顺序分别执行;
  • RouterChain - 这是一种很特别的 Chain,当你有多个 Chain 且不知道该使用哪个 Chain 处理用户请求时,可以使用它;首先,你给每个 Chain 取一个名字,然后给它们分别写一段描述,然后让大模型来决定该使用哪个 Chain 处理用户请求,这就被称之为 路由(route)

在这四个基础 Chain 中,RouterChain 是最复杂的一个,而且它一般不单独使用,而是和 MultiPromptChainMultiRetrievalQAChain 一起使用,类似于下面这样:

1
2
3
4
5
6
chain = MultiPromptChain(
router_chain=router_chain,
destination_chains=destination_chains,
default_chain=default_chain,
verbose=True,
)

MultiPromptChain 中,我们给定了多个目标 Chains,然后使用 RouterChain 来选取一个最适合处理用户请求的 Chain,如果不存在,则使用一个默认的 Chain,这种思路为我们打开了一扇解决问题的新大门,如果我们让大模型选择的不是 Chain,而是一个函数,或是一个外部接口,那么我们就可以通过大模型做出更多的动作,完成更多的任务,这不就是 OpenAI 的 Function Calling 功能吗?

实际上,LangChain 也提供了 create_structured_output_chain()create_openai_fn_chain() 等方法来创建 OpenAI Functions Chain,只不过 OpenAI 的 Function Calling 归根结底仍然只是一个 LLMChain,它只能返回要使用的函数和参数,并没有真正地调用它,如果要将大模型的输出和函数执行真正联系起来,这就得 Agents 出马了。

Agents 是 LangChain 里一个非常重要的主题,我们将在下一篇笔记中继续学习它。

参考

LangChain 官方资料

LangChain 项目

LangChain 教程

LangChain 可视化

LlamaIndex 参考资料

其他

自然语言处理

  1. 自然语言处理简介
    • 自然语言处理的概念, 难点, 应用, 意义 (为什么要自然语言处理)
      • 概念: 利用计算机对自然语言进行各种加工处理, 信息提取及应用的技术. 包括自然语言理解和自然语言生成.
      • 难点:
        • 词法: 分词歧义 (南京市长江大桥), 同词不同词性与词义, 缩略语等
        • 句法: 结构歧义 (咬死了猎人的狗)
        • 篇章: 指代不明 (张三看到了李四, 当时他正在用餐)
        • 语义: 上下文依赖 (你是什么意思? 小意思, 只是意思意思)
      • 应用: 文本分类, 情感分析, 信息抽取, 信息检索, 推荐系统, 问答系统, 对话系统, 文本生成
      • 意义: 自然语言丰富多样, 是人类表达思想, 传递信息的重要载体. 语言智能是人类智能的重要表现, 语言智能被誉为人工智能皇冠上的明珠
      • 内涵,外延
      • 可能会涉及到
  2. 基于规则的自然语言处理
    • 介绍
      • 1990 年以前是规则方法和专家系统的时代
      • 思想: 以规则形式表示语言知识, 基于规则的知识表示和推理, 强调人对语言知识的理性整理 (知识工程), 受计算语言学理论指导, 语言规则 (数据) 与程序分离, 程序体现为规则语言的解释器.
      • 词法分析: 形态还原 (英文), 分词 (中文), 词性标注, 命名实体识别
      • 简单了解规则方法的发展脉络
      • 它的思想是怎么样的
      • 一些经典的规则方法比如中文分词
    • 中文分词:方法、歧义以及如何消歧
      • 分词: 分词是指根据某个分词规范, 把一个 "字" 串划分成 "词" 串.
      • 它怎么处理中文分词
      • 歧义:
        • 交集型歧义 (和平等), 组合型歧义 (马上), 混合型歧义 (得到达)
        • 伪歧义字段指在任何情况下只有一种切分 (为人民), 根据歧义字段本身就能消歧
        • 真歧义字段指在不同的情况下有多种切分 (从小学), 根据歧义字段的上下文来消歧
      • 通过一些典型样例对规则方法有基本的了解
      • 分词方法
        • 正向最大匹配 (FMM) 或逆向最大匹配 (RMM)
          • 从左至右 (FMM) 或从右至左 (RMM), 取最长的词
        • 双向最大匹配
          • 分别采用 FMM 和 RMM 进行分词, 能发现 交集型歧义 ("幼儿园/地/节目"和"幼儿/园地/节目")
          • 如果结果一致, 则认为成功; 否则, 采用消歧规则进行消歧
        • 正向最大, 逆向最小匹配
          • 正向采用 FMM, 逆向采用最短词, 能发现 组合型歧义 ("他/骑/在/马上"和"他骑/在/马/上")
        • 逐词遍历匹配
          • 在全句中取最长的词, 去掉之, 对剩下字符串重复该过程
        • 设立切分标记
          • 收集词首字和词尾字, 先把句子分成较小单位, 再用某些方法切分
        • 全切分
          • 获得所有可能的切分, 选择最可能的切分
      • 消岐
        • 利用歧义字串, 前驱字串和后继字串的句法, 语义和语用信息
          • 句法信息
            • "阵风": 根据前面是否有数词来消歧. "一/阵/风/吹/过/来", "今天/有/阵风"
          • 语义信息
            • "了解": "他/学会/了/解/数学/难题" ("难题"一般是"解"而不是"了解")
          • 语用信息
            • "拍卖": "乒乓球拍卖完了", 要根据场景 (上下文) 来确定
        • 规则的粒度
          • 基于具体的词 (个性规则)
          • 基于词类, 词义类 (共性规则)
        • 统计方法
          • 根据词频消除交集型分词歧义?
    • 规则方法的优点与缺点
      • 优点:
        • 可以应用于缺乏足够标注数据的 NLP 任务
        • 大部分规则方法执行性能较好
        • 非黑箱模型便于人类理解
        • 可靠性比较强
      • 缺点:
        • 规则质量依赖于语言学家的知识和经验, 获取成本高
        • 规则之间容易发生冲突
        • 大规模规则系统维护难度大
  3. 语言模型和词向量
    • 统计语言模型的定义、参数估计、平滑以及公式推导等
      • 定义: 语言模型是衡量一句话出现在自然语言中的概率的模型
        • 数学形式上,给定一句话 \(s = \{ w_1, \ldots, w_n \}\),它对应的概率为 \(P(s) = P(w_1, \ldots, w_n) = \prod_{i=1}^{n} P(w_i | w_1, \ldots, w_{i-1})\)
      • 核心: 语言模型的核心在于根据前文预测下一个词出现的概率
        • \(P(w_i | w_1, \ldots, w_{i-1}), w_i \in V, V = \{ w_1, \ldots, w_{|V|} \}\)
      • 马尔可夫假设
        • 马尔可夫链: 描述从一个状态到转换另一个状态的随机过程. 该过程具备 "无记忆" 的性质, 即当前时刻状态的概率分布只能由上一时刻的状态决定, 和更久之前的状态无关
          • \(P(w_i | w_1, \ldots, w_{i-1}) = P(w_i | w_{i-1})\)
        • K 阶马尔可夫链: 当前时刻状态的概率分布只和前面 \(k\) 个时刻的状态相关
          • \(P(w_i | w_1, \ldots, w_{i-1}) = P(w_i | w_{i-k}, \ldots, w_{i-1})\)
        • 马尔可夫假设: 当前词出现的概率只和它前面的 \(k\) 个词相关
      • 用频率估计概率 (大数定理)
        • \(\displaystyle P(w_i | w_1, \ldots, w_{i-1}) = \frac{\operatorname{count}(w_{i-k}, \ldots, w_{i-1}, w_{i})}{\operatorname{count}(w_{i-k}, \ldots, w_{i-1})}\)
      • 拉普拉斯平滑
        • 数据稀疏问题 (分子): \(\operatorname{count}(w_{i-k}, \ldots, w_{i-1}, w_{i}) = 0\)
        • \(\displaystyle P(w_i | w_1, \ldots, w_{i-1}) = \frac{\operatorname{count}(w_{i-k}, \ldots, w_{i-1}, w_{i}) + 1}{\operatorname{count}(w_{i-k}, \ldots, w_{i-1}) + |V|}\)
      • 回退策略
        • 数据稀疏问题 (分母): \(\operatorname{count}(w_{i-k}, \ldots, w_{i-1}) = 0\)
        • \(\displaystyle P(w_i | w_1, \ldots, w_{i-1}) = \frac{\operatorname{count}(w_{i-k+j}, \ldots, w_{i-1}, w_{i})}{\operatorname{count}(w_{i-k+j}, \ldots, w_{i-1})}\)
      • 参数规模问题: 随着 \(k\) 增大, 参数数目指数增长, 无法存储
      • 困惑度 (Perplexity)
        • 用来度量一个概率分布或概率模型预测样本的好坏程度
        • 可以用来比较两个概率模型, 低困惑度的概率模型能更好地预测样本
        • \(\operatorname{Perplexity}(s) = 2^{H(s)} = 2^{-\frac{1}{n} \log_2 P(w_1, \ldots, w_n)} = \sqrt[n]{1 / P(w_1, \ldots, w_n)}\)
        • 在测试数据上计算困惑度
    • 离散词表示 vs 连续词表示
      • 离散词表示
        • One-hot 表示: 当前词维度为 1, 其它词的维度为 0
        • 词袋模型 (BOW, Bag of Words): 类似 One-hot, 但是是统计文本中每个单词出现的频率
        • 词频 (Term Frequency, TF): 在文档中出现频率越高的词对当前文档可能越重要
          • \(\displaystyle f_{ij} = \frac{\operatorname{count}(\text{term } i) \text{ in doc } j}{\operatorname{count}(\text{all term})\text{ in doc } j}\)
          • 归一化: \(\displaystyle tf_{ij} = \frac{f_{ij}}{\max_{k} (f_{kj})}\)
        • 逆文档频率 (Inverse Document Frequency, IDF): 在很多文档中都出现的词可能不重要 (如虚词)
          • \(df_{i} = \text{doc frequency of term } i = \text{numbers of doc containing term } i\)
          • \(\displaystyle idf_{i} = \log_2 \frac{N}{df_{i}}\)
        • TF-IDF: 综合一个词在当前文档中的频率和所有文档中出现的次数来度量这个词对当前文档的重要性
          • \(\displaystyle tf_{ij}\text{-}idf_{i} = tf_{ij} \cdot idf_{i} = tf_{ij} \cdot \log_2 \frac{N}{df_{i}}\)
        • 离散词表示的优点:
          • 得到的矩阵常常是稀疏矩阵
          • 算法容易理解且解释性较强
          • TF-IDF 可以衡量不同词对文本的重要性
        • 离散词表示的缺点:
          • 不能反映词的位置信息
          • 语义鸿沟: 没有对近义词间的联系进行建模
          • 维度爆炸: \(|V|\) 非常大, 导致存储, 计算开销极大
    • 分布式词表示的核心思想
      • 词向量 (word vectors, word embeddings): 用一个低维稠密的向量表示单词的整体含义
      • 分布式词表示核心思想: 一个词的含义能被这个词所在的上下文反映
        • 现代自然语言处理研究的奠基思想
      • 分布式词表示的优点
        • 每一维不表示具体的含义, 利用整体来表达含义
        • 低维: 节省存储空间, 节省计算时间
        • 可以度量词之词之间的语义相似性
    • CBOW 和 Skip-gram 的异同
      • 连续词典模型 (CBOW): 利用单词 \(c\) 的上下文 \(o\) 预测单词 \(c\)
        • \(P(w_i | w_{i-2}, w_{i-1}, w_{i+1}, w_{i+2})\)
      • Skip-gram: 利用单词 \(c\) 预测单词 \(c\) 的上下文 \(o\)
        • \(P(w_{i-2} | w_i), P(w_{i-1} | w_i), P(w_{i+1} | w_i), P(w_{i+2} | w_i)\)
    • word2vec 如何加速训练
      • Word2vec 是一套学习词向量的算法框架
      • 算法思想
        • 大量的自然语言文本 (训练语料)
        • 为词表中的每个词随机初始化一个向量表示
        • 遍历文本中的每个单词 \(c\), 其上下文单词为 \(o\)
        • 使用单词 \(c\) 的上下文 \(o\) 预测单词 \(c\) 的概率分布 (核心思想)
        • 更新词向量的表示使得单词 \(c\) 的预测概率最大化
      • 最小化对数似然损失函数: \(\displaystyle J(\theta) = - \frac{1}{n} \sum_{i=1}^{n} \log P(w_i | w_o; \theta)\)
      • 上下文 \(w_o\) 词向量: \(\displaystyle v_o = \frac{v_{i-k} + \cdots + v_{i-1} + v_{i+1} + \cdots + v_{i+k}}{2k}\)
      • 上下文 \(w_o\)\(w_i\) 概率: \(\displaystyle P(w_i | w_o; \theta) = \frac{\exp(v_o^{T}v_i)}{\sum_{j=1}^{|V|} \exp(v_o^{T}v_j)} = - u_o^{T}v_i + \log \sum_{j=1}^{|V|} \exp(u_o^{T}v_j)\)
      • 其中度量 \(v_o\)\(v_i\) 的相关性: \(v_o^{T}v_i\)
      • Softmax 的困境: 分母要遍历词表中所有的词
      • 模型优化: 负采样 (Negative Sampling)
        • 不再遍历词表中的所有词
        • 随机采样一些噪音词作为负样本, 通过正样本和负样本之间的对比学习让模型知道当前位置应该是放什么词
        • 损失函数分为 正样本最大化负样本最小化
        • \(\displaystyle J(\theta) = -\log \sigma(u_o^{T}v_i) - \sum_{m \in \{ M \text{ sampled indices} \}} \log \sigma(-u_o^{T}v_m)\)
      • 缺点
        • 词和向量是一对一的关系, 无法解决多义词的问题
        • word2vec 是一种静态的模型, 虽然通用性强, 但无法真的特定的任务做动态优化
    • GloVe 的核心思想
      • 共现矩阵
        • 代表方法: LSA, HAL
        • 优点: 速度快, 有效利用统计数据
        • 缺点: 过分依赖单词共现性和数据量
        • 实际上共现矩阵也分为基于窗口的共现矩阵和基于文档的共现矩阵, 前者也可以学习到局部信息
      • 直接学习
        • 代表方法: Skip-gram / CBOW
        • 优点: 能捕获语法和语义信息
        • 缺点: 速度和数据规模相关, 未有效利用统计数据
      • GloVe 模型既使用了语料库的全局统计 (overall statistics) 特征, 也使用了局部的上下文特征 (即滑动窗口)
      • 共现概率矩阵 \(X_{ij}\), 单词词向量 \(v_i, v_j\)
      • \(\displaystyle J = \sum_{i, j = 1}^{|V|} f(X_{ij}) (v_i^{T}v_j + b_i + b_j - \log X_{ij})^{2}\)
      • 优点: 训练快, 适应于大规模数据, 在小规模数据上性能优秀
  4. 神经网络和语言模型
    • 介绍
      • 为什么需要非线性激活函数: 使神经网络具有非线性拟合能力
      • 为什么需要偏置项: 权重参数 \(W\) 用于缩放拟合, 偏置项 \(b\) 用于平移拟合
    • 网络类型
      • 语言模型: 一句话出现的概率
        • \(P(s) = P(w_1, \ldots, w_n) = \prod_{i=1}^{n} P(w_i | w_1, \ldots, w_{i-1})\)
      • 基于 N-GRAM 的神经网络语言模型 (马尔可夫假设的 N-gram 模型的优化)
        • 优点: 不会有稀疏性问题, 不需要存储所有的 n-grams
        • 不足: 视野有限, 无法建模长距离语义; 窗口越大, 参数规模越大
      • 循环神经网络 (Recurrent Neural Network, RNN)
        • 优点: 重复使用隐层参数, 可处理任意序列长度
        • 优点: 能够使用历史信息, 模型参数量不随序列长度增加
        • 缺点: 逐步计算,速度较慢, 长期依赖问题
        • \(h_t = f(W_h h_{t-1} + W_{x} x_t + b)\)
      • 长短期记忆网络 ( Long Short-Term Memory, LSTM)
        • 引入三个门和一个细胞状态来控制神经元的信息流动
        • 遗忘门 \(f_t\): 控制哪些信息应该从之前的细胞状态中遗忘
        • 输入门 \(i_t\): 控制哪些信息应该被更新到细胞状态中
        • 输出门 \(o_t\): 控制哪些信息应该被输出到隐层状态中
        • 细胞状态 \(C_t\): 容纳神经元信息
      • 门控循环单元 (Gated Recurrent Unit, GRU)
        • 不使用细胞状态
      • 双向循环神经网络 (BI-RNN)
        • 正向 RNN 与反向 RNN 同时捕捉输入序列的上下文语义
      • 多层循环神经网络 (STACKED-RNN)
        • 捕捉输入序列的深层语义信息
    • 深层网络的问题以及如何缓解
      • 神经网络都有可能会产生梯度问题
        • 前馈神经网络, 卷积神经网络, 循环神经网络......
      • 深层网络更容易产生梯度问题
        • 梯度连乘后容易接近 0 (消失) 或者爆炸
      • 梯度消失常用解决方案
        • ReLU, 残差连接...
      • 梯度爆炸常用解决方案
        • 梯度裁剪 (Clipping)
  5. 高级神经网络和预训练模型
    • 编码器解码器
      • 机器翻译
        • 机器翻译 (Machine Translation) 是一个将源语言的句子 \(x\) 翻译成目标语言句子 \(y\) 的任务.
        • 神经机器翻译 (Neural Machine Translation): 用端到端 (end-to-end) 的神经网络来求解机器翻译任务.
      • 编码器-解码器框架 (Encoder-decoder)
        • Seq2Seq
        • 编码器 (encoder): 用来编码源语言的输入
        • 解码器 (decoder): 用来生成目标语言的输出
        • 通用性: 文本摘要, 对话生成, 代码生成
      • 问题
        • 编码整个源端句子的信息, 初始状态容易成为信息承载的瓶颈
        • 翻译过程不具有可解释性
        • 目标端解码某个特定单词时, 应该重点关注源端相关的单词
    • 注意力机制的设计目标与应用领域
      • 注意力机制: 目标端解码时, 直接从源端句子捕获对当前解码有帮助的信息, 从而生成更相关, 更更准确的解码结果
      • 优点:
        • 缓解 RNN 中的信息瓶颈问题
        • 缓解长距离依赖问题
        • 具有一定的可解释性
      • 计算注意力
        • 编码器-解码器: 编码器隐层状态 \(h_i\) 与解码器状态 \(s_t\) 内积得到注意力打分 \(e^{t} = [s_t^{T}h_1, \ldots, s_t^{T}h_N]\), 再用 softmax 转为注意力权重 \(\alpha^{t}\), 最后得到编码器隐层加权输出 \(a^{t} = \sum_{i=1}^{N}\alpha_i^{t}h_i\)
        • 一般情况: 神经网络隐层状态 \(h_i\) 与查询向量 \(s\) 使用打分函数得到注意力 \(e\), 再用 softmax 转为注意力权重 \(\alpha^{t}\), 最后得到编码器隐层加权输出 \(a^{t} = \sum_{i=1}^{N}\alpha_i^{t}h_i\)
      • 注意力打分函数
        • 向量内积: \(e_i = s^{T}h_i\)
        • 双线性变换: \(e_i = s^{T}Wh_i\)
        • 简单神经网络 (感知机): \(e_i = s^{T}W_1 + h_i^{T}W_2\)
      • 应用
        • 文本分类模型: 注意力可视化可以找到对情感标签影响大的单词
        • 文本摘要, 视觉问答, 语音识别
      • 提出来为了干什么,它能够用在哪些任务,在这些任务分别能够起到什么作用
    • Transformer 的组件、设计原理,尤其是 position embedding
      • https://luweikxy.gitbook.io/machine-learning-notes/self-attention-and-transformer
      • RNN 问题:
        • 有限的信息交互距离: 无法很好地解决长距离依赖关系, 不能很好地建模序列中的非线性结构关系
        • 无法并行: RNN 的隐层状态具有序列依赖性, 时间消耗随序列长度的增加而增加
      • 位置编码
        • 为什么要加入: 注意力计算方式是加权和, 无法考虑相对位置关系, 所以为了让模型能利用序列的顺序信息, 必须输入序列中词的位置
        • 将位置编码 \(p_i\) 注入到输入编码中: \(x_i = x_i + p_i\), 这说明我们需要将位置信息编码为一个维数与 \(x_i\) 相同的向量 \(p_i\)
        • 表示方法
          • 三角函数表示: 直接根据正余弦函数计算位置编码
            • 不需要从头学习, 直接计算得出
          • 从头学习: 随机初始化位置编码 \(p_i\), 并跟随网络一起训练
            • 能更好地拟合数据
      • 自注意力机制
        • Source 和 Target 相同的 Attention
        • \(Q = XW^{Q}, K = XW^{K}, V = XW^{V}\)
        • \(\displaystyle \operatorname{Attention}(Q, K, V) = \operatorname{softmax}\left( \frac{QK^{T}}{\sqrt{d_k}} \right)V\)
        • 除以 \(\sqrt{d_k}\): 防止维数过高时的值过大, 导致 softmax 函数反向传播时发生梯度消失
      • 多头自注意力机制
        • 并行地计算多个自注意力过程, 并拼接输出结果后, 通过一个线性层 (矩阵乘积乘以 \(W\)) 变为单个
        • 不同的 Self-Attention Head 以不同的理解方式计算注意力, 可以得到不同方面的结果
        • 有助于网络捕捉到更丰富的特征/信息
      • 残差连接
        • 将浅层网络和深层网络相连, 有利于梯度回传
        • 使深处网络的训练变得更加容易
      • 层正则
        • 使用均值和方差进行归一化
        • 对输入进行标准化
        • 加速收敛, 提升模型训练的稳定性
      • 前馈网络
        • 两层前馈神经网络
        • \(\operatorname{FFN}(X) = \max(0, XW_1 + b_1) W_2 + b_2\)
      • 解码器
        • Cross Attention: 解码时需要关注源端信息
        • Masked Attention: 解码时 (训练) 不应该看到未来的信息
      • 多层
        • 随着层数的增加, 网络的容量更大, 表达能力也更强
      • Transformer 的结构肯定是要了解的
      • 为什么要一定要这样一层层堆起来
      • 为什么要做这样的设计
      • 如果它没有用,去掉不就好了?
      • 要有一定的了解,要有理解的过程
    • 序列到序列问题实际建模
      • 可能出一个现实的场景,你会怎么建模它,参数估计,怎么去预测
  6. 文本分类
    • 朴素贝叶斯概念及计算
      • 朴素贝叶斯是生成式概率模型, 具有 "朴素" 假设, 适用于离散分布
      • \(\argmax_{y} P(y|x) = \argmax_{y} P(x, y) = \argmax_{y} P(x|y)P(y)\)
      • 朴素假设: 特征之间相互独立, 即任意两个词出现的概率互不影响
        • \(P(x, y) = P(w_1, \ldots, w_n | y) = \prod_{i=1}^{n} P(w_i|y) P(y)\)
      • 其中 \(P(w_i|y)\) 与具体的概率文档表示模型有关
      • 已标注的数据集 \(D\)
        • \(N\): 数据集 \(D\) 中的文档总数
        • \(N_k\): 数据集 \(D\) 中标签为 \(c_k\) 的文档数目
        • \(n_k(w_t)\): 标签为 \(c_k\) 的文档中, 包含单词 \(w_t\) 的文档数目
        • \(n_{it}\): 单词 \(w_t\) 在第 \(i\) 个文档中出现的次数
        • \(n_i\): 第 \(i\) 个文档包含的总单词数
        • \(z_{ik}\): 如果第 \(i\) 个文档的标签为 \(c_k\), 则 \(z_{ik} = 1\), 否则 \(z_{ik} = 0\)
      • 伯努利文档模型
        • 表示文本时只考虑单词是否出现, 不考虑出现次数
        • 参数估计: \(\displaystyle \hat{P}(w_t|c_k) = \frac{n_k(w_t)}{N_k}, \hat{P}(c_k) = \frac{N_k}{N}\)
        • 拉普拉斯平滑: \(\displaystyle \hat{P}(w_t|c_k) = \frac{n_k(w_t) + 1}{N_k + 2}, \hat{P}(c_k) = \frac{N_k + 1}{N + |Y|}\)
        • \(\argmax_{c_k} P(c_k|x) = \argmax_{c_k} P(x|c_k) P(c_k) = \argmax_{c_k} P(c_k) \prod_{t=1}^{|V|} (d_t P(w_t | c_k) + (1-d_t)(1-P(w_t|c_k)))\)
        • 缺点: 忽略了词频对文本分类的重要性
      • 多项式文档模型
        • 参数估计: \(\displaystyle \hat{P}(w_t|c_k) = \frac{\sum_{i=1}^{N}n_{it}Z_{ik}}{\sum_{j=1}^{|V|}\sum_{i=1}^{N}n_{it}Z_{ik}}, \hat{P}(c_k) = \frac{N_k}{N}\)
        • 拉普拉斯平滑: \(\displaystyle \hat{P}(w_t|c_k) = \frac{n_k(w_t) + 1}{N_k + |V|}, \hat{P}(c_k) = \frac{N_k + 1}{N + |Y|}\)
        • 注意这里的 \(N_k\)\(k\) 类中词的总数
        • \(\displaystyle \argmax_{c_k} P(c_k|x) = \argmax_{c_k} P(x|c_k) P(c_k) = \argmax_{c_k} P(c_k) \frac{n_i!}{\prod_{t=1}^{|V|} n_{it}!}\prod_{t=1}^{|V|} P(w_t|c_k)^{n_{it}} = \argmax_{c_k} P(c_k) \prod_{t=1}^{|V|} P(w_t|c_k)^{n_{it}}\)
    • 逻辑斯蒂回归、softmax 回归等概念与相关计算
      • 感知机
        • \(v\) 为文本 \(x\) 的特征表示
        • \(\omega\) 为特征表示 \(v\) 的权重向量
        • \(\hat{y}\) 为文本 \(x\) 的预测标签
        • 模型: \(\hat{y} = \begin{cases} 1, & \omega^{T}v \ge 0 \\ 0, & \omega^{T}v < 0 \end{cases}\)
        • 损失函数: \(\displaystyle J = \sum_{x_i \in c_0}\omega^{T}v_i - \sum_{x_j \in c_1}\omega^{T}v_j = \sum_{i=1}^{N}(\hat{y}_i - y_i) \omega^{T}v_i\)
        • 参数更新: \(\omega := \omega + \alpha(y - \hat{y})v = \begin{cases} \omega + \alpha v, & y = 1 \land \hat{y} = 0 \\ \omega - \alpha v, & y = 0 \land \hat{y} = 1 \\ \omega, \text{otherwise} \end{cases}\)
      • LOGISTIC 回归
        • 逻辑回归是一种二分类模型
        • 逻辑回归是一种线性分类模型
        • 用一个非线性激活函数 (Sigmoid 函数) 来模拟后验概率
        • Sigmoid 函数
          • \(\displaystyle \delta(z) = \frac{1}{1 + e^{-z}}\)
          • \(\displaystyle \frac{\mathrm{d} \delta(z)}{\mathrm{d}z} = \delta(z) (1-\delta(z))\)
        • 模型建模
          • \(\displaystyle P(y=1|x) = \delta(\omega^{T}v) = \frac{1}{1 + e^{-\omega^{T}v}}\)
          • \(\displaystyle P(y|x) = (\frac{1}{1 + e^{-\omega^{T}v}})^{y}(1 - \frac{1}{1 + e^{-\omega^{T}v}})^{1-y}\)
          • 似然函数: \(\displaystyle L = \prod_{i=1}^{N} P(y_i|x_i) = (\frac{1}{1 + e^{-\omega^{T}v_i}})^{y_i}(1 - \frac{1}{1 + e^{-\omega^{T}v_i}})^{1-y_i}\)
      • 多分类感知机
        • \(\omega\) 为特征表示 \(v\) 的权重向量
        • 模型: \(\hat{y} = \argmax_{j=1, \ldots, m} \omega_j^{T}v\)
        • 损失函数: \(\displaystyle J = \sum_{i=1}^{N}(\omega_{\hat{y}_i}^{T}v_i - \omega_{y_i}^{T}v_i)\)
        • 参数更新: \(\displaystyle \omega_j := \omega_j + \alpha(I(C=y_i) - I(c_j = \hat{y}_i))v_i = \begin{cases} \omega_j + \alpha v_i, & c_j = y_i \neq \hat{y}_i \\ \omega_j - \alpha v_i, & c_j = \hat{y}_i \neq y_i \\ \omega_j, & \text{otherwise} \end{cases}\)
      • SOFTMAX 回归
        • Softmax 回归是一种多分类模型, 也叫做多分类逻辑回归
        • 模型: \(\displaystyle \hat{y} = \argmax_{c_j = c_1, \ldots, c_m} P(c_j | x) = \argmax_{c_j = c_1, \ldots, c_m} \frac{e^{\omega_j^{T}v}}{\sum_{k=1}^{m}e^{\omega_k^{T}v}}\)
        • 似然函数: \(\displaystyle L = \prod_{i=1}^{N} P(y_i|x_i) = \prod_{i=1}^{N} \frac{e^{\omega_{y_i}^{T}v}}{\sum_{k=1}^{m}e^{\omega_k^{T}v}}\)
        • 损失函数: \(\displaystyle \mathcal{L} = -\frac{1}{N}\log L = -\frac{1}{N}\sum_{i=1}^{N} \log \frac{e^{\omega_{y_i}^{T}v}}{\sum_{k=1}^{m}e^{\omega_k^{T}v}}\)
        • 梯度
          • \(\displaystyle \frac{\delta \mathcal{L}}{\delta \omega_t} = -\frac{1}{N} \frac{\delta \sum_{i=1}^{N}h_{y_i}(x_i)}{\delta \omega_t} = \begin{cases} -\frac{1}{N}\sum_{i=1}^{N}(1-h_t(x_i))v_i, & t = y_i \\ -\frac{1}{N}\sum_{i=1}^{N}-h_t(x_i)v_i, & t \neq y_i \end{cases}\)
          • \(\displaystyle \frac{\delta \mathcal{L}}{\delta \omega_t} = -\frac{1}{N} \sum_{i=1}^{N}(I(t = y_i) - h_t(x_i))v_i\)
          • 误差 \(\times\) 特征
    • 卷积神经网络
      • 卷积操作 (Convolution)
        • 在图像中, 用于获取具有位置不变性的特征
        • 使用相同的卷积核 (kernel, 也叫滤波器 filter) 对不同位置进行卷积
        • 可以使用补齐 (padding) 和多个卷积核的文本卷积
        • 不同卷积核用于获取不同类型的特征
      • 池化操作 (Pooling)
        • 为什么能做池化: 池化不会改变目标对象的整体信息
        • 为什么需要做池化: 池化能使目标对象更小, 节省网络参数
        • 一种非线性形式的降采样
        • 一定程度上控制了过拟合
        • 最大池化和平均池化
    • 评价指标
      • 准确率 (Accuracy): 分类正确的测试样本比例
        • 问题: 对稀缺标签的分类性能不敏感
      • 精确率 (Precision): 对于所有预测结果为某标签的样本, 其中预测正确的样本所占的比例
      • 召回率 (Recall): 对于所有真实标签为某标签的样本,其中预测正确的样本所占的比例
      • F-score: 精确率和召回率的调和平均
      • Macro F-score
        • 先计算每个类的 F-score, 然后将它们平均
        • Precision 和 Recall 较高的类别对 F-score 的影响会较大
      • Micro F-score
        • 计算所有类别总的 TP, FP, FN, 然后计算总的 Precision 和 Recall, 最后再计算 F-score
        • 数量较多的类别对 Micro F-score 的影响会较大
  7. 序列化标注
    • 词性标注: 给定句子 \(X\), 求句子对应的词性序列 \(Y\)
    • 隐含马尔可夫概念及其计算
      • 马尔可夫链: 描述在状态空间中, 从一个状态到另一个状态转换的随机过程
      • 马尔科夫假设: 马尔可夫链在任意时刻 \(t\) 的状态只依赖于它在前一时刻的状态, 与其他时刻的状态无关
      • HMM 是一阶马尔可夫链的扩展
        • 状态序列不可见 (隐藏)
        • 隐藏的状态序列满足一阶马尔可夫链性质
        • 可见的观察值与隐藏的状态之间存在概率关系
      • 序列化标注的统计学模型: 描述了由隐马尔可夫链随机生成观测序列的过程, 属于生成模型
      • 时序概率模型: 描述由一个隐藏的马尔可夫链随机生成不可观测的状态随机序列, 再由各个状态生成一个观测值, 从而产生观测序列的过程
      • HMM 三元组
        • \(\lambda = (A, B, \pi)\)
        • 状态转移概率矩阵 \(A\): 表示状态之间的转移概率
        • 发射概率矩阵 \(B\): 表示某个状态下生成某个观测值的概率
        • 初始状态概率 \(\pi\): 开始时刻状态的概率分布
      • 三类问题
        • 概率计算: 已知 \(\lambda\) 计算 \(P(X|\lambda)\)
        • 模型学习 (参数估计): 已知 \(X\) 学习 \(\lambda = \argmax_{\lambda} P(X|\lambda)\)
        • 预测 (解码): 已知 \(\lambda\)\(X\) 解码出 \(Y = \argmax_{Y} P(Y | X, \lambda)\)
      • 概率计算
        • 暴力算法
        • 前向算法
          • 定义到 \(t\) 时刻部分观测序列为 \(x_1, \ldots, x_t\) 且状态为 \(q_i\) 的概率为前向概率 \(\alpha_{t}(i) = P(x_1, \ldots, x_t, y_t = q_i | \lambda)\)
          • \(\displaystyle \alpha_1(i) = \pi_i b_i(x_1), \alpha_t(i) = [\sum_{j=1}^{Q} \alpha_{t-1}(j) a_{j, i}]b_i(x_t), Q(X|\lambda) = \sum_{i=1}^{Q}\alpha_n(i)\)
          • 计算复杂度 \(O(n \times Q^{2})\)
        • 后向算法
          • 定义 \(t\) 时刻状态为 \(q_i\) 的条件下, \(t+1\) 时刻到 \(n\) 时刻部分观测序列为 \(x_{t+1}, \ldots, x_n\) 概率为后向概率 \(\beta_{t}(i) = P(x_{t+1}, \ldots, x_n | y_t = q_i, \lambda)\)
          • \(\displaystyle \beta_{n}(i) = 1, \beta_{t}(i) = \sum_{j=1}^{Q}a_{i,j}b_j(x_{t+1})\beta_{t+1}(j), P(X|\lambda) = \sum_{i=1}^{Q}\pi_i \beta_1(i) b_i(x_1)\)
        • 其他概率
          • \(\alpha_{t}(i)\beta_{t}(i) = P(y_t = q_i, X| \lambda)\)
          • \(\displaystyle \gamma_t(i) = P(y_t = q_i | X, \lambda) = \frac{\alpha_{t}\beta_{t}(i)}{\sum_{j=1}^{Q}\alpha_{t}(j)\beta_{t}(j)}\)
          • \(\alpha_t(i) a_{ij} b_j(x_{t+1}) \beta_{t+1}(j) = P(y_t = q_i, y_{t+1} = q_j, X | \lambda)\)
          • \(\zeta_t(i, j) = P(y_t = q_i, y_{t+1} = q_j | X, \lambda)\)
      • 参数估计
        • 监督学习: 训练数据包括观测序列和对应的状态序列, 通过监督学习来学习隐马尔可夫模型
          • 使用频率估计概率
        • 无监督学习: 训练数据仅包括观测序列, 通过无监督学习来学习隐马尔可夫模型
          • Baum-Welch 算法: 迭代式地使用 \(\gamma_{t}(i), \zeta_{t}(i, j)\)\(t=1, \ldots, n\) 上求和进行估计
      • 模型预测 (解码)
        • 维特比算法
          • \(\delta_{t}(i) = \max_{y_1, \ldots, y_{t-1}} P(y_1, \ldots, y_{t-1}, y_t = q_i, x_1, \ldots, x_t) = \max_{1\le j \le Q} \delta_{t-1}(j) \times a_{j,i} \times b_i(x_t)\)
          • \(\Psi_t(i) = \argmax_{1 \le j \le Q} \delta_{t-1}(j) a_{j,i}\)
          • 最优路径回溯: \(y_t^{*} = \Psi_{t+1}(y_{t+1}^{*})\)
      • 问题
        • 由于观测独立性假设 (任意时刻的观测只依赖于该时刻的马尔可夫链的状态), 很难融入更多的特征 (如上下文) 以表示复杂的关系
        • Label bias 问题: 由于马尔可夫假设使得在计算转移概率时做了局部归一化, 算法倾向于选择分支较少的状态
      • 我们举了很多的样例,这些问题可以用 HMM 解决
      • 因为隐藏在背后的就是序列化标注问题
    • 条件随机场
      • 条件随机场 (Conditional Random Fields, CRF) 是一种判别式模型
      • 直接建模条件概率
      • 定义一个特征函数集合, 用这个特征函数集合来为一个标注序列打分, 并据此选出最合理的标注序列
      • 线性链 CRF 可以看成序列化的 Softmax 回归, 都是判别式模型
      • 模型可以融合各种手工设计的特征, 引入领域知识
      • BILSTM-CRF: 在 BiLSTM 的基础上添加 CRF, 建模序列依赖性
    • 其他任务转化为序列化标注任务
      • 序列化标注: 中文分词
        • \(B\) 表示一个词的开始
        • \(I\) 表示一个的内部或结束
      • 序列化标注: 命名实体识别
        • 标签集合为 {B, I} × {PER, LOC, ORG} + {O}
        • \(B\)\(I\) 用来表示命名实体的开始位置, 中间或结束位置
        • PER, LOC, ORG 表示当前命名实体为人民, 地名, 或机构名
        • O 表示非命名实体片段
  8. 句法分析
    • 句法分析
      • 句法: 一门语言里支配句子结构, 决定词, 短语, 从句等句子成分如何组成其上级成分, 直到组成句子的规则或过程
      • 句法分析: 确定句子的组成, 词, 短语以及它们之间的关系
      • 句法分析类型
        • 成分句法分析 (Constituency Parsing)
        • 依存句法分析 (Dependency Parsing)
      • 成分句法分析
        • 研究词如何构成短语, 短语如何构成句子
      • 依存句法分析 (Dependency Parsing)
        • 研究词之间的依赖 (或支配) 关系
        • 依存是有向的: 词与词之间的依赖关系是二元不对称的 ("箭头"), "箭头" 头部指向的词依赖 "箭头" 尾部指向的词 (称为依存头)
        • 依存边是有类型的: 表明两个词之间的依赖关系类型, 如主语 (sub), 宾语 (obj) 等
        • 每个词只有一个依存头: 没有环, 依存关系是树结构
      • 应用: 语法检查, 信息抽取, 问答系统, 机器翻译
      • 主要是了解
      • 怎么让 SQL 写得更有效率,可能和句法有一定关系
      • 主要知道成分句法分析和依存句法分析大概在做什么事情
    • 成分句法分析算法
      • 树库
        • 带用句法结构注释的句子集合
        • 构建句法分析器, 可重复使用
        • 用于评价句法分析器的性能
        • 从树库中抽取句法规则
      • 上下文无关文法: 由非终结符集合 \(N\), 终结符集合 \(T\), 开始符号 \(S\), 产生式规则集合 \(R\) 构成的文法
      • 乔姆斯基范式 (CNF): 一种特殊的上下文无关文法, 产生式右侧只有一项或两项
      • 自顶向下分析方法的问题
        • 需要搜索的树数量达到指数级别
        • 许多子树是相同的, 存在重复解析
    • CKY 计算
      • 自底向上的动态规划算法
      • 具体计算方式看课件
      • 复杂度: \(O(n^{3}|G|)\)
      • 算法前提: 上下文无关文法 CFG 转换为乔姆斯基范式 CNF
      • CKY 的问题: 存在歧义
    • PCFG 计算
      • 概率上下文无关文法 PCFG
        • 由非终结符集合 \(N\), 终结符集合 \(T\), 开始符号 \(S\), 产生式规则集合 \(R\) 构成的文法
        • 加入了 \(P(X \to Y_1 Y_2 \cdots Y_n)\), 即产生式对应的概率
      • 利用 PCFG 计算一棵句法树 \(t\) 的概率
        • 句法树 \(t\) 通过以下产生式得到
          • \(\alpha_1 \to \beta_1, \ldots, \alpha_n \to \beta_n\)
        • 句法树 \(t\) 的概率为
          • \(p(t) = \prod_{i=1}^{n} p(\alpha_i \to \beta_i)\)
      • 概率 CKY 算法
        • 句子 \(s\) 对应的概率最大的句法树以及最大概率 bestscore(s)
      • PCFG 中的概率估计
        • 利用树库中的统计频率估计产生式概率
      • 怎样的树结构在 PCFG 下概率最大
      • 稍微知道怎么计算
    • 移进规约算法
      • 用于依存句法分析
      • 分析过程是一个自底向上的动作序列生成过程
        • 一个栈, 一个输入缓冲区, 一个依存边集合
      • 类似于 shift-reduce 分析, 只是在归约时, 增加左归约/右归约, 表示两个节点的依赖方向
      • 看课件示例
      • 如何决定每一步的动作: 统计学习, 深度学习
    • 评价方法
      • 完全匹配
        • 将句法分析器得到的树结构与人工标注 (树库) 的树结构进行完成匹配
        • 指标很低: 一棵树只要一条边不正确, 整棵树会被判断预测错误
        • 关键的边预测正确即可
      • 部分匹配
        • 预测正确的边数相对于标注边数的占比
        • UAS: unlabeled attachment score (不考虑边类型)
        • LAS: labeled attachment score (考虑边类型)
  9. 序列生成 - 机器翻译
    • 机器翻译的思想、深度学习建模
      • 机器翻译 (Machine Translation) 是一个将源语言的句子 \(x\) 翻译成目标语言句子 \(y\) (译文) 的任务
      • 理性主义: 以生成语言学为基础, 依靠人类先验知识
        • 基于规则的机器翻译
      • 经验主义: 以数据驱动为基础, 从数据中学习经验和知识
        • 基于实例的机器翻译
        • 统计机器翻译
        • 神经机器翻译
      • 基于规则的机器翻译
        • 分析
          • 将源语言句子解析成一种深层的结构表示
        • 转换
          • 将源语言句子的深层结构表示转换成目标语言的深层结构表示
        • 生成
          • 根据目标语言的深层结构表示生成对应的目标语言句子
        • 规则方法的问题
          • 规则质量依赖于语言学家的知识和经验, 获取成本高
          • 大规模规则系统维护难度大
          • 规则之间容易发生冲突
      • 基于实例的机器翻译
        • 从双语语料库中学习翻译实例
          • 利用类比思想 (analogy), 避免复杂的结构分析
          • 从语料库中查找与待翻译句子相近的实例, 通过逐词替换进行翻译
        • 基于实例翻译的问题
          • 检索的实例粒度一般为句子, 无法支持较长文本的翻译
          • 实例相似度较低时翻译欠佳
      • 统计机器翻译
        • 翻译目标: 将源语言的句子 \(x\) 翻译成目标语言句子 \(y\) (译文)
        • 噪声信道模型 (NOISY CHANNEL)
          • 生成式模型
            • 描述源语言的句子 \(x\) 是如何由目标语言句子 \(y\) 生成的
            • 原始信号 \(y\) 经过 "噪声" 扰动, 变成了观测信号 \(x\)
            • 根据观测信号 \(x\), 推测出原始信号 \(y\)
            • \(\argmax_{y} P(y|x) = \argmax_{y} P(y) P(x|y)\)
            • 其中 \(P(y)\) 称为源模型, \(P(x|y)\) 称为信道模型
          • 语言模型 \(P(y)\)
            • 对于由 \(m\) 个单词构成的目标语言句子 \(y = y_1 \cdots y_m\), 其语言模型概率 \(P(y)\)
              • \(P(y) = P(y_1)P(y_2|y_1) \cdots P(y_m|y_1 \cdots y_{m-1})\)
            • 可用统计语言模型建模 (ngram 模型)
          • 翻译模型 \(P(x|y)\)
            • IBM Model: 引入对齐概念来帮助计算翻译模型
            • 假设源语言句子 \(x\) 和目标语言句子 \(y\) 之间的一种对齐方式为 \(a = a_1 \cdots a_n\), 其中 \(a_n \in [0, 1, \ldots, m]\), \(n\)\(m\) 分别为源语言句子和目标语言句子长度, 则: \(P(x|y) = \sum_{a} P(x, a|y)\)
            • \(P(x, a|y) = P(n|y) P(a|y,n) P(x|y,a,n) = P(n|y) \prod_{j=1}^{n} (P(a_j|a_1\cdots a_{j-1}, x_1 \cdots x_{j-1}, y, n) \times P(x_j | a_1 \cdots a_{j-1}, x_1 \ldots x_{j-1}, y, n))\)
            • 分别为长度生成模型, 词语对齐模型, 词对翻译模型
            • IBM Model 1 假设
              • \(P(n|y) = c\), 即长度生成概率是一个常量
              • \(a_j \sim \operatorname{uniform}(0, \ldots, m)\), 即词语对齐模型服从均匀分布, 词语对齐概率 \(\displaystyle P(a_j | a_1 \cdots a_{j-1}, x_1 \cdots x_{j-1}, y, n) = \frac{1}{m + 1}\)
              • \(x_j \sim \operatorname{Categorical}(\theta_{y_{a_j}})\), 即词对翻译概率服从类别分布, 词对翻译概率 \(P(x_j | a_1 \cdots a_{j-1}, x_1 \cdots x_{j-1}, y, n) = P(x_j|y_{a_j})\)
            • \(\displaystyle P(x, a|y) = \frac{c}{(m+1)^{n}}\prod_{j=1}^{n} p(x_j|y_{a_j})\)
            • \(\displaystyle P(x|y) = \sum_{a} P(x|y) = \frac{c}{(m+1)^{n}}\prod_{j=1}^{n} \sum_{i=0}^{m} p(x_j|y_{a_j})\)
          • 解码出 \(y\) 使 \(P(y)P(x|y)\) 最大
            • Viterbi 算法
      • 神经机器翻译
        • 神经机器翻译 (Neural Machine Translation): 用端到端 (end-to-end) 的神经网络来建模机器翻译任务
        • 编码器-解码器框架 (Encoder-decoder)
          • Seq2Seq
          • 编码器 (encoder): 用来编码源语言的输入
          • 解码器 (decoder): 用来生成目标语言的输出
        • 基于 RNN 的编码器-解码器框架
          • 编码器 RNN 编码了源端句子的上下文信息
          • 解码器 RNN 是一个给定输入编码条件下的语言模型
          • 问题
            • 编码整个源端句子的信息, 初始状态容易成为信息承载的瓶颈
            • 翻译过程不具有可解释性
            • 目标端解码某个特定单词时, 应该重点关注源端相关的单词
        • 基于注意力机制的编码器-解码器框架
          • 在编码器 RNN 与解码器 RNN 的基础上加入注意力机制
        • 基于 RNN 的编码器-解码器问题
          • 有限的信息交互距离: 无法很好地解决长距离依赖关系, 不能很好地建模序列中的非线性结构关系
          • 无法并行: RNN 的隐层状态具有序列依赖性, 时间消耗随序列长度的增加而增加
        • TRANSFORMER 翻译
      • 肯定要知道序列到序列建模的思想
      • 还有深度学习怎么建模这种序列到序列的问题
      • 很多问题都可以转换到序列到序列
        • 分类:目标序列只有一个字符
    • 贪心解码与柱搜索
      • 贪心解码 (GREEDY SEARCH)
        • 解码时, 每个时刻从解码器取出概率最大的词 (argmax) 作为预测结果
        • 问题: 某一时刻的错误翻译会影响后续所有翻译, 因此不一定能解码出全局最佳 (译文概率最大) 的译文
      • 枚举解码
        • 解码时枚举所有的翻译结果 \(y\)
        • 极其耗时, 复杂度 \(O(V^{T})\), \(V\) 为目标语言的词表大小, \(T\) 为生成译文的长度
      • 柱搜索解码 (BEAM SEARCH)
        • 在贪心搜索基础上扩大搜索空间, 从而更有可能解码出全局最优的译文
        • 核心思想: 在 \(t\) 时刻, 保留 \(k\) 个概率最大的翻译结果 (k 为 beam size)
          • 相比于枚举, 极大提高搜索效率
          • 并不能保证一定解码出最优的译文
        • 打分函数: \(\operatorname{score}(y_1, \ldots, y_t) = \log P(y_1, \ldots, y_t | x) = \sum_{i=1}^{t}\log P(y_i|y_1, \ldots, y_{i-1}, x)\)
      • 直接贪心解码会有什么问题
      • 柱搜索它是怎么运行起来的,为什么要柱搜索
    • BLEU 的定义、计算、存在的问题
      • 机器翻译质量评估
        • 人工评估
          • 评估准则 (打分制)
            • 忠实度: 译文对源文信息和语义的保留程度 ("信")
            • 流利度: 衡量译文是否流畅通顺 ("达")
          • 缺点: 主观偏差, 成本昂贵, 效率低
        • 自动评估
          • 核心思想
            • 通过比较机器翻译的译文和参考译文 (人工翻译结果) 之间的相似程度来衡量翻译结果的好坏
            • 机器译文越接近人工翻译结果, 其译文的质量就越好
      • BLEU
        • 一种衡量机器翻译质量的自动评估指标
        • 统计机器翻译译文与参考译文中 \(n\) 元文法匹配的数目占系统译文中所有 \(n\) 元文法总数的比例, 即 \(n\) 元文法的精确率
        • 符号定义
          • \(y^{*}\): 源语言句子 \(x\) 对应的机器翻译译文
          • \((y_1, \ldots, y_{M})\): \(M\) 个人工参考译文
          • \(\operatorname{count}_{\text{match}}(\text{ngram})\): 某 ngram 片段在 \(y^{*}\)\((y_1, \ldots, y_{M})\) 中共同出现的最大次数 (将系统译文和参考译文逐个对比并统计共现次数, 将共现最大值作为结果)
          • \(\operatorname{count}(\text{ngram})\): ngram 片段在 \(y^{*}\) 中出现的次数
        • 计算所有 ngram 的精确率 \(p_n\)
          • \(n\) 为 ngram 长度, 最大值一般取 4 (\(n\) 增大时, ngram 共现次数呈指数级下降)
          • \(\displaystyle p_n = \frac{\sum_{y^{*}}\sum_{\text{ngram} \in y^{*}} \operatorname{count}_{\text{match}}(\text{ngram})}{\sum_{y^{*}}\sum_{\text{ngram} \in y^{*}} \operatorname{count}(\text{ngram})}\)
          • 即分子是每一个 ngram 在参考译文中的出现次数之和, 重数只能在一篇参考译文里面找, 分母是每一个 ngram 的出现次数之和
          • \(n\) 最大值取 \(4\) 时, \(p_1, p_2, p_3, p_4\) 加权精确率为 \(\displaystyle \prod_{n=1}^{4} p_{n}^{w_n}\), 通常 \(w_n = \frac{1}{4}\)
        • 精确率 \(p_n\) 的问题
          • \(p_n\) 的计算公式中分母为机器翻译译文, 译文越短, \(p_n\) 的值倾向更大
          • 具有偏向性, 对于漏翻的词不敏感
        • 引入长度惩罚因子 \(BP\), 对过短的译文进行惩罚
          • \(c\): 测试语料中每个源语言句子对应的系统译文 \(y^{*}\) 的长度
          • \(r\): 测试语料中每个源语言句子对应的多个参考译文 \((y_1, \ldots, y_n)\) 中最短译文或者与 \(y^{*}\) 长度最接近的参考译文的长度
          • \(\displaystyle BP = \begin{cases} 1, & c > r \\ e^{1-\frac{r}{c}}, & c \le r \end{cases}\)
          • 综合 ngram 匹配精确率和长度惩罚因子, BLEU 评分公式为:
          • \(\displaystyle BLEU = BP \times \prod_{n=1}^{4} p_n^{w_n} = BP \times \exp(\sum_{n=1}^{4} w_n \log p_n)\)
        • 优点:
          • 自动评估
          • 能够从 ngram 角度评估翻译质量
          • 计算速度快, 计算成本低, 容易理解, 与具体语言无关, 和人类给的评估高度相关
        • 缺点
          • 它不考虑意义
          • 它不直接考虑句子结构
          • 它不能很好地处理形态丰富的语言
          • 它与人类的判断并不相符
          • 不考虑语言表达 (语法) 上的准确性; 测评精度会受常用词的干扰; 短译句的测评精度有时会较高; 没有考虑同义词或相似表达的情况, 可能会导致合理翻译被否定
        • 改进
          • ROUGE 可以看做是 BLEU 的改进版, 专注于召回率而非精度
          • NIST 方法是在 BLEU 方法上的一种改进. 最主要的是引入了每个 n-gram 的信息量 (information) 的概念. BLEU 算法只是单纯的将 n-gram 的数目加起来, 而 NIST 是在得到信息量累加起来再除以整个译文的 n-gram 片段数目. 这样相当于对于一些出现少的重点的词权重就给的大了
          • 和 BLEU 不同, METEOR 同时考虑了基于整个语料库上的准确率和召回率, 而最终得出测度
      • 怎么去评价序列生成问题
      • 现有评价方式是否还存在问题
      • 我们能否改进它
  10. 自然语言处理范式演进
    • 演进
      • 规则范式
        • 优点
          • 利用人类专家总结的规则, 对特定领域精确度高
        • 缺点
          • 规则质量依赖于语言学家的知识和经验, 获取成本高
          • 规则有限, 泛化性, 鲁棒性差
          • 一致性差, 规则之间容易发生冲突
          • 扩充困难, 大规模规则系统维护难度大
      • 统计学习范式 (特征工程)
        • 优点
          • 有较好的鲁棒性和泛化性
        • 缺点
          • 繁重的特征工程
          • 特征表示: 语义 gap, 维度爆炸
      • 深度学习范式
        • 面向任务的模型设计 (结构工程)
          • 端到端学习
            • 不需要复杂的特征工程
          • 结构工程
            • 特征工程 ---> 结构工程
            • 归纳偏置 (Inductive Bias)
            • 探究最适配下游任务的网络结构
        • 预训练+微调 (pre-train+fine-tune) (目标工程)
          • 预训练 (Pre-train)
            • 从海量无标注文本中学习通用的语法和语义知识
            • Transformer 解码器 (生成式)
            • 预训练任务: 下一个词预测 (Language Model)
            • 如果模型能够准确预测互联⽹上所有⽂本⽚段的下⼀个单词, 那么模型就理解了文字本身
          • 微调 (Fine-tune)
            • 在下游任务的标注数据上微调, 利用预训练学到的语义语法知识辅助下游任务
            • 将下游任务拼接成统一的文本格式, 并用解码器的最后一个 token 进行分类
            • 微调-分类
              • 新闻分类, 情感分类, 垃圾邮件检测......
              • 输入: 单个句子
              • 输出: 利用 token [CLS] 的表示进行分类
            • 微调-匹配
              • 自然语言推断, 搜索......
              • 输入: 两个句子, 用 [SEP] 分隔
              • 输出: 利用 token [CLS] 的表示进行分类
            • 微调-序列化标注
              • 中文分词, 命名实体识别......
              • 输入: 单个句子
              • 输出: 对句子中每个 token 的表示进行分类
            • 微调-机器阅读理解
              • 阅读理解, 机器问答......
              • 输入: 问题和上下文, 用 [SEP] 分隔
              • 输出: 答案在上下文中的开始/结束位置
        • 预训练+提示+预测 (pre-train+prompt+predict) (提示工程)
        • ChatGPT
    • Prompt 的核心思想、为什么能 work
      • 预训练+微调 (目标工程)
        • 预训练语言模型 ➔ 下游任务
          • 形式 gap: 将预训练的语言学习目标调整为适合下游任务的学习目标
          • 语义 gap: 无法利用预训练阶段学习的词预测层, 需要学习额外的任务分类层
      • 深度学习范式---预训练+提示+预测 (提示工程)
        • 下游任务 ➔ 预训练语言模型
          • 将所有下游任务的学习目标转换成语言模型形式
          • 好处: 预训练目标和下游任务目标形式一致, 没有形式和语义 gap
        • 核⼼思想: 将下游任务的标签转换为由上下⽂预测的单词, 将所有下游任务统一成预训练任务, 以特定的模板,将下游任务的数据转成自然语言形式
      • 深度学习范式---预训练+提示+预测
        • 预训练 (Pre-train)
          • 从海量无标记文本中学习语法和语义知识
        • 提示 (Prompt)
          • 设计 prompt, 将下游任务转换成语言模型进行训练
        • 预测 (Predict)
          • 利用预训练语言模型对下游任务进行预测
      • Prompt
        • Prompt 是一种通过向输入文本添加额外的提示文本, 从而更好地利用预训练模型知识的技术
      • Prompt 工作流
        • Prompt 工程
          • 设计合适的 prompt (提示文本), 将下游任务转换为对应的语言模型任务
        • Answer 工程
          • 设计对应的单词-标签映射, 将上述语言模型预测出来的词映射到对应的类别空间
          • 映射表: (answer→label) fantastic→Positive
      • Prompt 工程
        • Cloze Prompt (完形填空 prompt): 适合 Masked Language Model, 如 BERT
          • I love this movie. Overall, it is a [] movie.
        • Prefix Prompt (前缀 prompt): 适合 Language Model, 如 GPT 系列
          • I love this movie. Overall, this movie is []
        • 手工设计或自动搜索
      • Answer 工程
        • 输入 -> 提示 -> 预测 -> 答案 -> 标签
        • 情感分类
          • I love this movie. It was a [MASK] movie.
          • I love this movie. It was a good movie.
          • good -> Positive (答案 -> 标签)
        • 文本匹配 (自然语言推断)
          • A soccer game with multiple males playing.? [MASK], Some men are playing a sport.
          • A soccer game with multiple males playing.? Yes, Some men are playing a sport.
          • Yes: EntailmentNo: Contradiction
        • 序列化标注 (命名实体识别)
          • ACL will be held in Bangkok. Bangkok is a <MASK> entity
          • ACL will be held in Bangkok. Bangkok is a Location entity
          • Location: LocOrganization: Org
      • 零样本学习能力 (Zero-shot Learning)
        • 不使用任何下游任务的标注数据去微调预训练模型, 直接进行提示+预测
        • 模型也能取得不错的效果
      • 比较
        • 预训练+微调
          • 目标函数挖掘
            • 利用预训练模型作为骨干网络, 引入额外的目标函数以适配下游任务
          • 语言模型 → 下游任务
            • 语言模型的训练目标和下游任务的训练目标间有 gap
            • 输入和标签之间的映射函数需要较多数据训练
        • 预训练+提示+预测
          • 下游任务 → 语言模型
          • 更充分地利用预训练的语法和语义信息
          • 更适合低资源场景
            • 将输入和标签之间的映射函数转换成语言模型中的语义匹配, 需要的数据更少
      • Prompt 集成 (Prompt Ensembling)
      • 训练好大模型后,怎么激发模型的能力
      • 为什么会 work
        • 实际上, prompt 可以看做是对预训练语言模型中已经记忆的知识的一种检索方式, 由于 prompt 任务形式就是预训练任务, 所以相比于 fine-tuning, 当使用 prompt 形式向模型中输入样本时, 预测时得到了 "提示", 因此所需使用到的信息量是更多的. 因为要用到更多的信息量, 所以样本的类别分布在 prompt 视角之下都是稀疏的, 这应该也是 prompt 在 few-shot 上效果显著的原因.
    • 指令学习 (Instruction Tuning)
      • 构造各种不同任务的指令描述
      • 人工标注或半自动构造若干任务的提示样本和答案, 参与模型再训练 (有监督训练); 数据量百万级规模
      • 更好地激活了模型理解用户意图 (任务) 能力; 增强了任务本身的泛化能力 (小样本/零样本的能力)
    • 情境学习 (In-context Learning)
      • 一种使用特定格式 Prompt 的提示学习技术
    • 思维链 (Chain-of-Thought, CoT)
      • Demonstrations: 求解问题的推理示例, 包含推理过程
    • 能力涌现
      • 当模型规模到达一定数量 (参数量>10B), 经过提示工程的设计, 大模型在多个任务上, 表现出小样本情境学习上的涌现能力
      • 在需要推理过程的任务上有效, 例如数学推理, 常识推理以及符号推理等
    • Prompt 设计题
      • 可以设计我们自己的一个提示模板
    • Prompt Learning 和 Instruction Learning 的区别与联系
      • 指示学习: 针对每个任务, 单独生成 instruction (hard token), 通过在若干个 full-shot 任务上进行微调, 然后在具体的任务上进行评估泛化能力 (zero shot), 其中预训练模型参数是 unfreeze 的
      • Prompt 是激发语言模型的补全能力, 例如根据上半句生成下半句, 或是完形填空等. Instruct 是激发语言模型的理解能力, 它通过给出更明显的指令, 让模型去做出正确的行动. 指示学习的优点是它经过多任务的微调后, 也能够在其他任务上做 zero-shot, 而提示学习都是针对一个任务的. 泛化能力不如指示学习. 只是了解,不会太重点

前言

之前用的是博客园,但是那个账号密码老记不住,干脆自己搭建一个吧,好歹也是一个码农。 然后花了两个晚上加一个上午,通过hexogithub搭建了一个个人博客。 网上教程、文档那么多,为什么花这么久时间?当然是踩坑了啊。 所以下面会记录一些遇到的问题和坑。 如果你看完这边文章,那你只需要两个小时就能搭建成功。欢迎有兴趣的小伙伴尝试一下。

正文

环境准备

  1. node.js
  2. git

这两个应用windows用户直接搜索下载安装就可以。 如果习惯了使用linux命令的朋友,推荐windows神器cmder。 可以直接在windows环境下使用linux命令,样式可调,再也不要用黑乎乎的cmd了,而且自带git,完全可以不用下载windows git。

正式安装hexo

hexo官方中文文档

在node.js安装好的前提下,全局安装hexo 如何判断node.js是否安装成功?执行以下命令,如果能够看到版本号则说明安装成功了

1
node -v

安装hexo

1
npm install -g hexo-cli

自选合适的目录,新建文件夹<folder>

1
2
3
cd <folder>
hexo init
npm install

不再赘述,直接看官方文档。

配置github

新建仓库,仓库名必须为[your_name.github.io]

补充:本地配置github ssh连接,方便自动部署,以及clone你喜欢的主题(theme)

windows用户直接在c:/用户/youername/.ssh/下查看是否有id_rsa.pub文件。 没有的话命令行执行命令ssh-keygen -t rsa -C "your eamil",会自动生成id_rsa.pub文件,打开后复制。

github->头像->Settings→SSH kyes→Add SSH key,粘贴复制的内容。

配置本地账户

1
2
git config --global user.name “your_username” #设置用户名
git config --global user.email “your_email” #设置邮箱地址,最好使用注册邮箱地址

测试是否配置成功

1
ssh -T git@github.com

hexo配置以及使用

有两个配置文件: - 一个是根目录下的_config.yml称为站点配置文件 - 一个是themes/landscape/_config.yml称为主题配置文件(默认主题:landscape)

站点配置如下:

1
2
3
4
5
6
7
url: https://yourname.github.io/
theme: landscape #选择你想用的主题,我用的是indigo
deploy:
type: git # 不要使用github
repo: git@github.com:pengwenwu/pengwenwu.github.io.git # 使用ssh连接
branch: master # 默认master分支
message: add new blog # 自动部署commit备注,可不填

hexo常用命令

hexo命令参考

hexo n "我的博客" == hexo new "我的博客" #新建文章
hexo p == hexo publish
hexo g == hexo generate #生成
hexo s == hexo server #启动服务本地预览
hexo d == hexo deploy #部署
hexo clean #清除缓存 网页正常情况下可以忽略此条命令

hexo server #Hexo 会监视文件变动并自动更新,您无须重启服务器。
hexo server -s #静态模式
hexo server -p 5000 #更改端口
hexo server -i 192.168.1.1 #自定义 IP

在执行之前,记得安装自动部署 (--save 加不加的区别在于是否写入到依赖文件package.json中)

1
npm install hexo-deployer-git --save

正常本地预览,直接执行hexo s,如果要发布话最好执行clean命令,会去删除生成的public文件,完整部署命令:hexo clean && hexo g && hexo d。或者直接hexo d -g

注意问题

安装完自动部署后,是不需要本地git init新建仓库的。执行hexo g会在根目录生成public文件夹,自动部署, 本质是将public文件夹内容全部提交到仓库中去,默认会访问编译好的index.html。

如果部署完,访问your_name.github.io 404,可能有下面几个原因 1. 首先检查仓库文件,是不是全都是public的文件内容,如果整个本地blog文件夹都提交了,首先清空 仓库,然后删除本地.deploy_git文件夹,再重新部署 2. 文件有报错,本地hexo s观察是否有报错。

不喜欢原主题的朋友,可以github去找喜欢的主题。执行命令

1
git clone XXXX.next.git themes\next
这个会将新的主题下载到themes下对应的next目录,next为主题的名字。

主题的配置,可以看文档,修改对应的主题配置文件。
我使用的主题是indigo,详细文档indigo

markdown不会使用的朋友,参考链接markdown中文文档
如果没有ide的话,可以使用在线预览Cmd Markdown