聊天机器人(ChatRobot)的概念我们并不陌生,也许你曾经在百无聊赖之下和Siri打情骂俏过,亦或是闲暇之余与小爱同学谈笑风生,无论如何,我们都得承认,人工智能已经深入了我们的生活。目前市面上提供三方api的机器人不胜枚举:微软小冰、图灵机器人、腾讯闲聊、青云客机器人等等,只要我们想,就随时可以在app端或者web应用上进行接入。但是,这些应用的底层到底如何实现的?在没有网络接入的情况下,我们能不能像美剧《西部世界》(Westworld)里面描绘的那样,机器人只需要存储在本地的心智球就可以和人类沟通交流,如果你不仅仅满足于当一个调包侠,请跟随我们的旅程,本次我们将首度使用深度学习库KerasTensorFlow打造属于自己的本地聊天机器人,不依赖任何三方接口与网络。 首先安装相关依赖:pip3installTensorflowpip3installKeraspip3installnltkpip3installpandas 然后撰写脚本testbot。py导入需要的库:importnltkimportsslfromnltk。stem。lancasterimportLancasterStemmerstemmerLancasterStemmer()importnumpyasnpfromkeras。modelsimportSequentialfromkeras。layersimportDense,Activation,Dropoutfromkeras。optimizersimportSGDimportpandasaspdimportpickleimportrandom 这里有一个坑,就是自然语言分析库NLTK会报一个错误:Resourcepunktnotfound 正常情况下,只要加上一行下载器代码即可importnltknltk。download(punkt) 但是由于学术上网的原因,很难通过python下载器正常下载,所以我们玩一次曲线救国,手动自己下载压缩包:https:raw。githubusercontent。comnltknltkdataghpagespackagestokenizerspunkt。zip 解压之后,放在你的用户目录下即可:C:Usersliuyueokenizersltkdatapunkt ok,言归正传,开发聊天机器人所面对的最主要挑战是对用户输入信息进行分类,以及能够识别人类的正确意图(这个可以用机器学习解决,但是太复杂,我偷懒了,所以用的深度学习Keras)。第二就是怎样保持语境,也就是分析和跟踪上下文,通常情况下,我们不太需要对用户意图进行分类,只需要把用户输入的信息当作聊天机器人问题的答案即可,所这里我们使用Keras深度学习库用于构建分类模型。 聊天机器人的意向和需要学习的模式都定义在一个简单的变量中。不需要动辄上T的语料库。我们知道如果玩机器人的,手里没有语料库,就会被人嘲笑,但是我们的目标只是为某一个特定的语境建立一个特定聊天机器人。所以分类模型作为小词汇量创建,它仅仅将能够识别为训练提供的一小组模式。 说白了就是,所谓的机器学习,就是你重复的教机器做某一件或几件正确的事情,在训练中,你不停的演示怎么做是正确的,然后期望机器在学习中能够举一反三,只不过这次我们不教它很多事情,只一件,用来测试它的反应而已,是不是有点像你在家里训练你的宠物狗?只不过狗子可没法和你聊天。 这里的意向数据变量我就简单举个例子,如果愿意,你可以用语料库对变量进行无限扩充:intents{intents:〔{tag:打招呼,patterns:〔你好,您好,请问,有人吗,师傅,不好意思,美女,帅哥,靓妹,hi〕,responses:〔您好,又是您啊,吃了么您内,您有事吗〕,context:〔〕},{tag:告别,patterns:〔再见,拜拜,88,回见,回头见〕,responses:〔再见,一路顺风,下次见,拜拜了您内〕,context:〔〕},〕} 可以看到,我插入了两个语境标签,打招呼和告别,包括用户输入信息以及机器回应数据。 在开始分类模型训练之前,我们需要先建立词汇。模式经过处理后建立词汇库。每一个词都会有词干产生通用词根,这将有助于能够匹配更多用户输入的组合。forintentinintents〔intents〕:forpatterninintent〔patterns〕:tokenizeeachwordinthesentencewnltk。wordtokenize(pattern)addtoourwordslistwords。extend(w)addtodocumentsinourcorpusdocuments。append((w,intent〔tag〕))addtoourclasseslistifintent〔tag〕notinclasses:classes。append(intent〔tag〕)words〔stemmer。stem(w。lower())forwinwordsifwnotinignorewords〕wordssorted(list(set(words)))classessorted(list(set(classes)))print(len(classes),语境,classes)print(len(words),词数,words) 输出:2语境〔告别,打招呼〕14词数〔88,不好意思,你好,再见,回头见,回见,帅哥,师傅,您好,拜拜,有人吗,美女,请问,靓妹〕 训练不会根据词汇来分析,因为词汇对于机器来说是没有任何意义的,这也是很多中文分词库所陷入的误区,其实机器并不理解你输入的到底是英文还是中文,我们只需要将单词或者中文转化为包含01的数组的词袋。数组长度将等于词汇量大小,当当前模式中的一个单词或词汇位于给定位置时,将设置为1。createourtrainingdatatraining〔〕createanemptyarrayforouroutputoutputempty〔0〕len(classes)trainingset,bagofwordsforeachsentencefordocindocuments:initializeourbagofwordsbag〔〕patternwordsdoc〔0〕patternwords〔stemmer。stem(word。lower())forwordinpatternwords〕forwinwords:bag。append(1)ifwinpatternwordselsebag。append(0)outputrowlist(outputempty)outputrow〔classes。index(doc〔1〕)〕1training。append(〔bag,outputrow〕)random。shuffle(training)trainingnp。array(training)trainxlist(training〔:,0〕)trainylist(training〔:,1〕) 我们开始进行数据训练,模型是用Keras建立的,基于三层。由于数据基数小,分类输出将是多类数组,这将有助于识别编码意图。使用softmax激活来产生多类分类输出(结果返回一个01的数组:〔1,0,0,。。。,0〕这个数组可以识别编码意图)。modelSequential()model。add(Dense(128,inputshape(len(trainx〔0〕),),activationrelu))model。add(Dropout(0。5))model。add(Dense(64,activationrelu))model。add(Dropout(0。5))model。add(Dense(len(trainy〔0〕),activationsoftmax))sgdSGD(lr0。01,decay1e6,momentum0。9,nesterovTrue)model。compile(losscategoricalcrossentropy,optimizersgd,metrics〔accuracy〕)model。fit(np。array(trainx),np。array(trainy),epochs200,batchsize5,verbose1) 这块是以200次迭代的方式执行训练,批处理量为5个,因为我的测试数据样本小,所以100次也可以,这不是重点。 开始训练:1414〔〕0s32mssteploss:0。7305acc:0。5000Epoch22001414〔〕0s391ussteploss:0。7458acc:0。4286Epoch32001414〔〕0s390ussteploss:0。7086acc:0。3571Epoch42001414〔〕0s395ussteploss:0。6941acc:0。6429Epoch52001414〔〕0s426ussteploss:0。6358acc:0。7143Epoch62001414〔〕0s356ussteploss:0。6287acc:0。5714Epoch72001414〔〕0s366ussteploss:0。6457acc:0。6429Epoch82001414〔〕0s899ussteploss:0。6336acc:0。6429Epoch92001414〔〕0s464ussteploss:0。5815acc:0。6429Epoch102001414〔〕0s408ussteploss:0。5895acc:0。6429Epoch112001414〔〕0s548ussteploss:0。6050acc:0。6429Epoch122001414〔〕0s468ussteploss:0。6254acc:0。6429Epoch132001414〔〕0s388ussteploss:0。4990acc:0。7857Epoch142001414〔〕0s392ussteploss:0。5880acc:0。7143Epoch152001414〔〕0s370ussteploss:0。5118acc:0。8571Epoch162001414〔〕0s457ussteploss:0。5579acc:0。7143Epoch172001414〔〕0s432ussteploss:0。4535acc:0。7857Epoch182001414〔〕0s357ussteploss:0。4367acc:0。7857Epoch192001414〔〕0s384ussteploss:0。4751acc:0。7857Epoch202001414〔〕0s346ussteploss:0。4404acc:0。9286Epoch212001414〔〕0s500ussteploss:0。4325acc:0。8571Epoch222001414〔〕0s400ussteploss:0。4104acc:0。9286Epoch232001414〔〕0s738ussteploss:0。4296acc:0。7857Epoch242001414〔〕0s387ussteploss:0。3706acc:0。9286Epoch252001414〔〕0s430ussteploss:0。4213acc:0。8571Epoch262001414〔〕0s351ussteploss:0。2867acc:1。0000Epoch272001414〔〕0s3mssteploss:0。2903acc:1。0000Epoch282001414〔〕0s366ussteploss:0。3010acc:0。9286Epoch292001414〔〕0s404ussteploss:0。2466acc:0。9286Epoch302001414〔〕0s428ussteploss:0。3035acc:0。7857Epoch312001414〔〕0s407ussteploss:0。2075acc:1。0000Epoch322001414〔〕0s457ussteploss:0。2167acc:0。9286Epoch332001414〔〕0s613ussteploss:0。1266acc:1。0000Epoch342001414〔〕0s534ussteploss:0。2906acc:0。9286Epoch352001414〔〕0s463ussteploss:0。2560acc:0。9286Epoch362001414〔〕0s500ussteploss:0。1686acc:1。0000Epoch372001414〔〕0s387ussteploss:0。0922acc:1。0000Epoch382001414〔〕0s430ussteploss:0。1620acc:1。0000Epoch392001414〔〕0s371ussteploss:0。1104acc:1。0000Epoch402001414〔〕0s488ussteploss:0。1330acc:1。0000Epoch412001414〔〕0s381ussteploss:0。1322acc:1。0000Epoch422001414〔〕0s462ussteploss:0。0575acc:1。0000Epoch432001414〔〕0s1mssteploss:0。1137acc:1。0000Epoch442001414〔〕0s450ussteploss:0。0245acc:1。0000Epoch452001414〔〕0s470ussteploss:0。1824acc:1。0000Epoch462001414〔〕0s444ussteploss:0。0822acc:1。0000Epoch472001414〔〕0s436ussteploss:0。0939acc:1。0000Epoch482001414〔〕0s396ussteploss:0。0288acc:1。0000Epoch492001414〔〕0s580ussteploss:0。1367acc:0。9286Epoch502001414〔〕0s351ussteploss:0。0363acc:1。0000Epoch512001414〔〕0s379ussteploss:0。0272acc:1。0000Epoch522001414〔〕0s358ussteploss:0。0712acc:1。0000Epoch532001414〔〕0s4mssteploss:0。0426acc:1。0000Epoch542001414〔〕0s370ussteploss:0。0430acc:1。0000Epoch552001414〔〕0s368ussteploss:0。0292acc:1。0000Epoch562001414〔〕0s494ussteploss:0。0777acc:1。0000Epoch572001414〔〕0s356ussteploss:0。0496acc:1。0000Epoch582001414〔〕0s427ussteploss:0。1485acc:1。0000Epoch592001414〔〕0s381ussteploss:0。1006acc:1。0000Epoch602001414〔〕0s421ussteploss:0。0183acc:1。0000Epoch612001414〔〕0s344ussteploss:0。0788acc:0。9286Epoch622001414〔〕0s529ussteploss:0。0176acc:1。0000 ok,200次之后,现在模型已经训练好了,现在声明一个方法用来进行词袋转换:defcleanupsentence(sentence):tokenizethepatternsplitwordsintoarraysentencewordsnltk。wordtokenize(sentence)stemeachwordcreateshortformforwordsentencewords〔stemmer。stem(word。lower())forwordinsentencewords〕returnsentencewordsdefbow(sentence,words,showdetailsTrue):tokenizethepatternsentencewordscleanupsentence(sentence)bagofwordsmatrixofNwords,vocabularymatrixbag〔0〕len(words)forsinsentencewords:fori,winenumerate(words):ifws:assign1ifcurrentwordisinthevocabularypositionbag〔i〕1ifshowdetails:print(foundinbag:sw)return(np。array(bag)) 测试一下,看看是否可以命中词袋:pbow(你好,words)print(p) 返回值:foundinbag:你好〔00100000000000〕 很明显匹配成功,词已入袋。 在我们打包模型之前,可以使用model。predict函数对用户输入进行分类测试,并根据计算出的概率返回用户意图(可以返回多个意图,根据概率倒序输出):defclassifylocal(sentence):ERRORTHRESHOLD0。25generateprobabilitiesfromthemodelinputdatapd。DataFrame(〔bow(sentence,words)〕,dtypefloat,index〔input〕)resultsmodel。predict(〔inputdata〕)〔0〕filteroutpredictionsbelowathreshold,andprovideintentindexresults〔〔i,r〕fori,rinenumerate(results)ifrERRORTHRESHOLD〕sortbystrengthofprobabilityresults。sort(keylambdax:x〔1〕,reverseTrue)returnlist〔〕forrinresults:returnlist。append((classes〔r〔0〕〕,str(r〔1〕)))returntupleofintentandprobabilityreturnreturnlist 测试一下:print(classifylocal(您好)) 返回值:foundinbag:您好〔(打招呼,0。999913)〕liuyue:mytornadoliuyue 再测:print(classifylocal(88)) 返回值:foundinbag:88〔(告别,0。9995449)〕 完美,匹配出打招呼的语境标签,如果愿意,可以多测试几个,完善模型。 测试完成之后,我们可以将训练好的模型打包,这样每次调用之前就不用训练了:model。save(。v3u。h5) 这里分类模型会在根目录产出,文件名为v3u。h5,将它保存好,一会儿会用到。 接下来,我们来搭建一个聊天机器人的API,这里我们使用目前非常火的框架Fastapi,将模型文件放入到项目的目录之后,编写main。py:importrandomimportuvicornfromfastapiimportFastAPIappFastAPI()defclassifylocal(sentence):ERRORTHRESHOLD0。25generateprobabilitiesfromthemodelinputdatapd。DataFrame(〔bow(sentence,words)〕,dtypefloat,index〔input〕)resultsmodel。predict(〔inputdata〕)〔0〕filteroutpredictionsbelowathreshold,andprovideintentindexresults〔〔i,r〕fori,rinenumerate(results)ifrERRORTHRESHOLD〕sortbystrengthofprobabilityresults。sort(keylambdax:x〔1〕,reverseTrue)returnlist〔〕forrinresults:returnlist。append((classes〔r〔0〕〕,str(r〔1〕)))returntupleofintentandprobabilityreturnreturnlistapp。get()asyncdefroot(word:strNone):fromkeras。modelsimportmodelfromjson,loadmodelmodelloadmodel(。v3u。h5)wordlistclassifylocal(word)aforintentinintents〔intents〕:ifintent〔tag〕wordlist〔0〕〔0〕:arandom。choice(intent〔responses〕)return{message:a}ifnamemain:uvicorn。run(app,host127。0。0。1,port8000) 这里的:fromkeras。modelsimportmodelfromjson,loadmodelmodelloadmodel(。v3u。h5) 用来导入刚才训练好的模型库,随后启动服务:uvicornmain:appreload 效果是这样的: 结语:毫无疑问,科技改变生活,聊天机器人可以让我们没有佳人相伴的情况下,也可以听闻莺啼燕语,相信不久的将来,笑语盈盈、衣香鬓影的机械姬亦能伴吾等于清风明月之下。