前言

说明:讲解时会对相关文章资料进行思想、结构、优缺点,内容进行提炼和记录,相关引用会标明出处,引用之处如有侵权,烦请告知删除。
转载请注明:DengBoCong

构建对话机器人的现有方法中,可以分为 generation-based(生成式)和retrieval-based(检索式),相对于生成式而言,检索式拥有的信息更加丰富,且运行流畅的特点。本篇文章不具体讲解模型,而是来好好阐述关于检索候选回复的实现,比如SMN模型、DAM模型等中,关于检索候选回复的实现。关于SMN模型的论文笔记和实现代码可以参考我的另一篇文章GitHub,后续我还会对DAM论文和模型写一篇文章。

使用到的工具版本如下:

  • Solr:8.6.3
  • pysolr:3.9.0
  • python:3.7
  • CentOS:7.6
  • Docker:19.03.9

整体流程

我们讲解工具使用之前,首先简要的阐述一下我们的目的,如果已经了解过检索式对话系统或者阅读过相应论文,就不用看了。首先我们知道目的是检索候选回复,用什么检索呢?这个和具体模型结构和需求有关。拿SMN模型为例,利用启发式方法从索引中获取候选response,将前一轮的utterances ${u_1,…,u_{n-1}}$ (也就是对话的历史)和 $u_n$ 进行计算,根据他们的tf-idf得分,从 ${u_1,…,u_{n-1}}$ 中提取前 $5$ 个关键字,然后将扩展后的message用于索引,并使用索引的内联检索算法来检索候选response。

模型结构和训练至关重要,但是检索候选回复也是使得整个对话流程实现闭环的关键。我们了解了检索的目的和整体流程,那我们从何实现?方式有很多,可以自行编写一个脚本从数据集中生成一个索引候选数据集(这个是我最开始用的方法,但毕竟没专门研究过检索,所以写的很粗糙,勉强验证功能可以,用作正式使用就不行了),还有一种就是使用现有的检索工具,比如Lucene、Solr、ElasticSearch等等。所以这篇文章就是来讲解部署solr和使用python实现检索(为什么选用Solr?不是说那种工具好坏,而是佛系使用,貌似ElasticSearch现在很火的样子,哈哈哈)。

Solr和Pysolr

Solr它是一种开放源码的、基于 Lucene Java 的搜索服务器,易于加入到 Web 应用程序中。Lucene很底层,从底层代码层面来实现需求,而Solr在其上进行了封装,你如果想要实现脱机检索,那还是使用Lucene吧。Solr 提供了层面搜索(就是统计)、命中醒目显示并且支持多种输出格式(包括XML/XSLT 和JSON等格式)。它易于安装和配置,而且附带了一个基于 HTTP 的管理界面。Solr已经在众多大型的网站中使用,较为成熟和稳定。Solr 包装并扩展了 Lucene,所以Solr的基本上沿用了Lucene的相关术语。更重要的是,Solr 创建的索引与 Lucene 搜索引擎库完全兼容。通过对Solr 进行适当的配置,某些情况下可能需要进行编码,Solr 可以阅读和使用构建到其他 Lucene 应用程序中的索引。此外,很多 Lucene 工具(如Nutch、 Luke)也可以使用Solr 创建的索引。可以使用 Solr 的表现优异的基本搜索功能,也可以对它进行扩展从而满足企业的需要,Solr官网(官方将其和Lucene并列放在一起,嘿嘿嘿,万变不离其宗,看官方文档)。

而Pysolr是基于Python的Solr轻量级封装,它提供了服务器查询并返回基于查询的结果接口。简单来说就是Pysolr封装了Solr的各种http请求,使用起来非常方便,你可以直接从pypi中直接导入(这个就要吐槽一下Pylucene了,不能从pipy直接导入),PySolr官方地址,上面有使用的示例和API,可以自行去看,这里配一张Solr的示意图,方面后面理解:

在这里插入图片描述

部署Solr

部署

都0202年了,部署服务应用都是用容器了吧,我这里讲解用Docker部署solr,不了解的可以参考我的关于Docker的几篇文章,我这里就不介绍Docker了,默认会就接着往下讲了。

有了docker环境之后,首先先将solr拉下来,我这里拉的是8.6.3的版本(ps:不喜欢拉最新的,因为最新的可能其他的附属库跟不上更新,出问题)

1
2
3
4
5
6
7
docker pull solr:8.6.3
# 然后启动solr
docker run -itd --name solr -p 8983:8983 solr:8.6.3
# 然后创建core核心选择器,我这里因为以SMN模型讲解,所以取名SMN
# exec -it :交互式执行容器
# -c 内核的名称(必须)
docker exec -it --user=solr solr bin/solr create_core -c smn

指令具体含义以及core是啥,请自行查阅资料,或者研究一下solr,毕竟先学习基础再来实战。上面构建solr运行容器是简单粗暴且实用的方法,也可以和我一样使用Dockerfile进行构建镜像和容器,Dockerfile内容如下(内容来自docker-solr项目,官方的docker镜像项目):

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
FROM openjdk:11-jre

LABEL maintainer="The Apache Lucene/Solr Project"
LABEL repository="https://github.com/docker-solr/docker-solr"

ARG SOLR_VERSION="8.6.3"
ARG SOLR_SHA512="f040d4489118b655bd27451a717c1f22f180c398638d944a53889a1a449e7032b016cecbff1979c2e8bfd51fc037dd613f3b968254001d34fe0e8fc4f6761dcf"
ARG SOLR_KEYS="902CC51935C140BF820230961FD5295281436075"
# If specified, this will override SOLR_DOWNLOAD_SERVER and all ASF mirrors. Typically used downstream for custom builds
ARG SOLR_DOWNLOAD_URL

# Override the solr download location with e.g.:
# docker build -t mine --build-arg SOLR_DOWNLOAD_SERVER=http://www-eu.apache.org/dist/lucene/solr .
ARG SOLR_DOWNLOAD_SERVER

RUN set -ex; \
apt-get update; \
apt-get -y install acl dirmngr gpg lsof procps wget netcat gosu tini; \
rm -rf /var/lib/apt/lists/*; \
cd /usr/local/bin; wget -nv https://github.com/apangin/jattach/releases/download/v1.5/jattach; chmod 755 jattach; \
echo >jattach.sha512 "d8eedbb3e192a8596c08efedff99b9acf1075331e1747107c07cdb1718db2abe259ef168109e46bd4cf80d47d43028ff469f95e6ddcbdda4d7ffa73a20e852f9 jattach"; \
sha512sum -c jattach.sha512; rm jattach.sha512

ENV SOLR_USER="solr" \
SOLR_UID="8983" \
SOLR_GROUP="solr" \
SOLR_GID="8983" \
SOLR_CLOSER_URL="http://www.apache.org/dyn/closer.lua?filename=lucene/solr/$SOLR_VERSION/solr-$SOLR_VERSION.tgz&action=download" \
SOLR_DIST_URL="https://www.apache.org/dist/lucene/solr/$SOLR_VERSION/solr-$SOLR_VERSION.tgz" \
SOLR_ARCHIVE_URL="https://archive.apache.org/dist/lucene/solr/$SOLR_VERSION/solr-$SOLR_VERSION.tgz" \
PATH="/opt/solr/bin:/opt/docker-solr/scripts:$PATH" \
SOLR_INCLUDE=/etc/default/solr.in.sh \
SOLR_HOME=/var/solr/data \
SOLR_PID_DIR=/var/solr \
SOLR_LOGS_DIR=/var/solr/logs \
LOG4J_PROPS=/var/solr/log4j2.xml

RUN set -ex; \
groupadd -r --gid "$SOLR_GID" "$SOLR_GROUP"; \
useradd -r --uid "$SOLR_UID" --gid "$SOLR_GID" "$SOLR_USER"

RUN set -ex; \
export GNUPGHOME="/tmp/gnupg_home"; \
mkdir -p "$GNUPGHOME"; \
chmod 700 "$GNUPGHOME"; \
echo "disable-ipv6" >> "$GNUPGHOME/dirmngr.conf"; \
for key in $SOLR_KEYS; do \
found=''; \
for server in \
ha.pool.sks-keyservers.net \
hkp://keyserver.ubuntu.com:80 \
hkp://p80.pool.sks-keyservers.net:80 \
pgp.mit.edu \
; do \
echo " trying $server for $key"; \
gpg --batch --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$key" && found=yes && break; \
gpg --batch --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$key" && found=yes && break; \
done; \
test -z "$found" && echo >&2 "error: failed to fetch $key from several disparate servers -- network issues?" && exit 1; \
done; \
exit 0

RUN set -ex; \
export GNUPGHOME="/tmp/gnupg_home"; \
MAX_REDIRECTS=1; \
if [ -n "$SOLR_DOWNLOAD_URL" ]; then \
# If a custom URL is defined, we download from non-ASF mirror URL and allow more redirects and skip GPG step
# This takes effect only if the SOLR_DOWNLOAD_URL build-arg is specified, typically in downstream Dockerfiles
MAX_REDIRECTS=4; \
SKIP_GPG_CHECK=true; \
elif [ -n "$SOLR_DOWNLOAD_SERVER" ]; then \
SOLR_DOWNLOAD_URL="$SOLR_DOWNLOAD_SERVER/$SOLR_VERSION/solr-$SOLR_VERSION.tgz"; \
fi; \
for url in $SOLR_DOWNLOAD_URL $SOLR_CLOSER_URL $SOLR_DIST_URL $SOLR_ARCHIVE_URL; do \
if [ -f "/opt/solr-$SOLR_VERSION.tgz" ]; then break; fi; \
echo "downloading $url"; \
if wget -t 10 --max-redirect $MAX_REDIRECTS --retry-connrefused -nv "$url" -O "/opt/solr-$SOLR_VERSION.tgz"; then break; else rm -f "/opt/solr-$SOLR_VERSION.tgz"; fi; \
done; \
if [ ! -f "/opt/solr-$SOLR_VERSION.tgz" ]; then echo "failed all download attempts for solr-$SOLR_VERSION.tgz"; exit 1; fi; \
if [ -z "$SKIP_GPG_CHECK" ]; then \
echo "downloading $SOLR_ARCHIVE_URL.asc"; \
wget -nv "$SOLR_ARCHIVE_URL.asc" -O "/opt/solr-$SOLR_VERSION.tgz.asc"; \
echo "$SOLR_SHA512 */opt/solr-$SOLR_VERSION.tgz" | sha512sum -c -; \
(>&2 ls -l "/opt/solr-$SOLR_VERSION.tgz" "/opt/solr-$SOLR_VERSION.tgz.asc"); \
gpg --batch --verify "/opt/solr-$SOLR_VERSION.tgz.asc" "/opt/solr-$SOLR_VERSION.tgz"; \
else \
echo "Skipping GPG validation due to non-Apache build"; \
fi; \
tar -C /opt --extract --file "/opt/solr-$SOLR_VERSION.tgz"; \
(cd /opt; ln -s "solr-$SOLR_VERSION" solr); \
rm "/opt/solr-$SOLR_VERSION.tgz"*; \
rm -Rf /opt/solr/docs/ /opt/solr/dist/{solr-core-$SOLR_VERSION.jar,solr-solrj-$SOLR_VERSION.jar,solrj-lib,solr-test-framework-$SOLR_VERSION.jar,test-framework}; \
mkdir -p /opt/solr/server/solr/lib /docker-entrypoint-initdb.d /opt/docker-solr; \
chown -R 0:0 "/opt/solr-$SOLR_VERSION"; \
find "/opt/solr-$SOLR_VERSION" -type d -print0 | xargs -0 chmod 0755; \
find "/opt/solr-$SOLR_VERSION" -type f -print0 | xargs -0 chmod 0644; \
chmod -R 0755 "/opt/solr-$SOLR_VERSION/bin" "/opt/solr-$SOLR_VERSION/contrib/prometheus-exporter/bin/solr-exporter" /opt/solr-$SOLR_VERSION/server/scripts/cloud-scripts; \
cp /opt/solr/bin/solr.in.sh /etc/default/solr.in.sh; \
mv /opt/solr/bin/solr.in.sh /opt/solr/bin/solr.in.sh.orig; \
mv /opt/solr/bin/solr.in.cmd /opt/solr/bin/solr.in.cmd.orig; \
chown root:0 /etc/default/solr.in.sh; \
chmod 0664 /etc/default/solr.in.sh; \
mkdir -p /var/solr/data /var/solr/logs; \
(cd /opt/solr/server/solr; cp solr.xml zoo.cfg /var/solr/data/); \
cp /opt/solr/server/resources/log4j2.xml /var/solr/log4j2.xml; \
find /var/solr -type d -print0 | xargs -0 chmod 0770; \
find /var/solr -type f -print0 | xargs -0 chmod 0660; \
sed -i -e "s/\"\$(whoami)\" == \"root\"/\$(id -u) == 0/" /opt/solr/bin/solr; \
sed -i -e 's/lsof -PniTCP:/lsof -t -PniTCP:/' /opt/solr/bin/solr; \
chown -R "0:0" /opt/solr-$SOLR_VERSION /docker-entrypoint-initdb.d /opt/docker-solr; \
chown -R "$SOLR_USER:0" /var/solr; \
{ command -v gpgconf; gpgconf --kill all || :; }; \
rm -r "$GNUPGHOME"

COPY --chown=0:0 scripts /opt/docker-solr/scripts

VOLUME /var/solr
EXPOSE 8983
WORKDIR /opt/solr
USER $SOLR_USER

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["solr-foreground"]

容器运行情况如下:
在这里插入图片描述
接下来可以访问:http://xxxxxx:8983/solr/,进入到solr界面,如下:
在这里插入图片描述
然后点击Core Admin,查看一下自己刚刚创建的Core,如下:
在这里插入图片描述
然后选择smn就可以使用了,如下:
在这里插入图片描述
结束了?当然没有,哪有那么简单的事儿,首先我们上面算是基本部署好了solr,但是我们需要进行一些必要的使得我们能更好的使用,比如我们需要对文档进行分词,添加相似度计算类(用于tf-idf计算),接下来就说明如何配置这两个东西。

配置IK

首先是IK,IK Analyzer(中文分词器)是一个开源的,基于java语言开发的轻量级的中文分词工具包。最初,它是以开源项目 Lucene为应用主体的,结合词典分词和文法分析算法的中文分词组件。新版本的IKAnalyzer3.0则发展为 面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。

  • Solr 5以前的可以装上老版本,提取码:g5ib
  • Solr 6使用这个版本
  • Solr 7&8使用这个版本

注意要将IK源码打成JAR包(作为一个老Java,打包还是不难的)。接着将jar包通过传输软件或其它方式传入宿主机的某一文件夹内,然后使用指令将jar包复制到Solr容器的分词包文件夹中:

1
docker cp ik-analyzer.jar solr:/opt/solr-8.6.3/contrib/analysis-extras/lucene-libs

查看 Solr 容器在宿主机中数据卷的位置:

1
2
3
4
docker inspect solr
# 找到 Mounts
# Destination : 容器里的路径
# Source : 对应宿主机里的路径

在这里插入图片描述
将IK分词器配置到 Solr 的核心配置文件中,Source为上面的Mounts中的:

1
2
cd {Source}/data/myIKCore/conf/
vim solrconfig.xml

然后添加如下内容:

1
2
3
# dir	容器存放自带分词JAR包的目录
# regex JAR包名
<lib dir="${solr.install.dir:../../../..}/contrib/analysis-extras/lucene-libs/" regex="ik-analyzer.jar" />

声明中文分词器

1
vim managed-schema

找到指定位置添加配置

1
2
3
4
5
6
7
<!-- IKAnalyzer -->
<fieldType name ="text_ik" class ="solr.TextField">
<!-- 索引时候的分词器 -->
<analyzer type ="index" isMaxWordLength ="false" class="org.wltea.analyzer.lucene.IKAnalyzer"/>
<!-- 查询时候的分词器 -->
<analyzer type ="query" isMaxWordLength ="true" class="org.wltea.analyzer.lucene.IKAnalyzer"/>
</fieldType>

重启 Solr 容器

1
docker restart solr

选择刚刚创建的核心选择器
在这里插入图片描述

配置相似度

到了这里,你其实可以直接用了,但是如果使用tf-idf的话,会报错,如下:

1
2
org.apache.solr.client.solrj.SolrServerException: No live SolrServers available to handle this request
null:java.lang.UnsupportedOperationException: requires a TFIDFSimilarity (such as ClassicSimilarity)

所以还是需要配置,打开 managed-schema 将下面一行添加进就可以了:

1
<similarity class="solr.ClassicSimilarityFactory"/>

pysolr使用

1
2
3
4
5
6
# 创建solr
solr = pysolr.Solr(url=solr_server, always_commit=True, timeout=10)
# 使用前习惯性安全检查
solr.ping()
# 将回复数据添加索引,responses是一个json,形式如:[{},{},{},...],里面每个对象构建按照你回复的需求即可
solr.add(docs=responses)

接下来进行查询,首先我们提取关键词词,我这里将我的tf-idf方法的代码贴出来,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def get_tf_idf_top_k(history: list, k: int = 5):
"""
使用tf_idf算法计算权重最高的k个词,并返回
Args:
history: 上下文语句
k: 返回词数量
Returns: top_5_key
"""
tf_idf = {}

vectorizer = TfidfVectorizer(analyzer='word')
weights = vectorizer.fit_transform(history).toarray()[-1]
key_words = vectorizer.get_feature_names()

for i in range(len(weights)):
tf_idf[key_words[i]] = weights[i]

top_k_key = []
tf_idf_sorted = sorted(tf_idf.items(), key=lambda x: x[1], reverse=True)[:k]
for element in tf_idf_sorted:
top_k_key.append(element[0])

return top_k_key

然后将得到的五个关键词通过query的语法进行组合,得到查询语句,我这里只返回前十个分数最高的候选回复:

1
2
3
4
5
6
7
query = "{!func}sum("
for keyin tf_idf:
query += "product(idf(utterance," + key + "),tf(utterance," + key + ")),"
query += ")"
candidates = self.solr.search(q=query, start=0, rows=10).docs

#query合起来长这样:{!func}sum(product(idf(utterance,key1),tf(utterance,key1),product(idf(utterance,key2),tf(utterance,key2),...)

查询回复格式如下:
在这里插入图片描述
然后检索得到了候选回复就可以喂给模型了。